Import un-converted framework libs from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Wed, 23 Dec 2009 01:25:08 +0000 (18:25 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Wed, 23 Dec 2009 01:25:27 +0000 (18:25 -0700)
592 files changed:
framework/Alarm/Alarm.php [new file with mode: 0644]
framework/Alarm/Alarm/sql.php [new file with mode: 0644]
framework/Alarm/package.xml [new file with mode: 0644]
framework/Alarm/tests/001.phpt [new file with mode: 0644]
framework/Alarm/tests/setup.inc.dist [new file with mode: 0644]
framework/DOM/DOM.php [new file with mode: 0644]
framework/DOM/package.xml [new file with mode: 0644]
framework/DOM/tests/dom_load_error.phpt [new file with mode: 0644]
framework/DOM/tests/fixtures/load_error.xml [new file with mode: 0644]
framework/DOM/tests/fixtures/load_ok.xml [new file with mode: 0644]
framework/Data/Data.php [new file with mode: 0644]
framework/Data/Data/csv.php [new file with mode: 0644]
framework/Data/Data/icalendar.php [new file with mode: 0644]
framework/Data/Data/imc.php [new file with mode: 0644]
framework/Data/Data/outlookcsv.php [new file with mode: 0644]
framework/Data/Data/tsv.php [new file with mode: 0644]
framework/Data/Data/vcard.php [new file with mode: 0644]
framework/Data/Data/vnote.php [new file with mode: 0644]
framework/Data/Data/vtodo.php [new file with mode: 0644]
framework/Data/docs/examples/Eudora.txt [new file with mode: 0644]
framework/Data/docs/examples/Gmail.csv [new file with mode: 0644]
framework/Data/docs/examples/KMail.csv [new file with mode: 0644]
framework/Data/docs/examples/Outlook.csv [new file with mode: 0644]
framework/Data/docs/examples/Palm.csv [new file with mode: 0644]
framework/Data/docs/examples/Thunderbird.csv [new file with mode: 0644]
framework/Data/docs/examples/WAB-selectable.csv [new file with mode: 0644]
framework/Data/docs/examples/WAB.csv [new file with mode: 0644]
framework/Data/docs/examples/Yahoo.csv [new file with mode: 0644]
framework/Data/package.xml [new file with mode: 0644]
framework/Data/tests/csv_importFile_01.phpt [new file with mode: 0644]
framework/Data/tests/simple_dos.csv [new file with mode: 0644]
framework/Data/tests/simple_unix.csv [new file with mode: 0644]
framework/DataTree/DataTree.php [new file with mode: 0644]
framework/DataTree/DataTree/null.php [new file with mode: 0644]
framework/DataTree/DataTree/sql.php [new file with mode: 0644]
framework/DataTree/docs/find-datatree-attribute-orphans.sql [new file with mode: 0644]
framework/DataTree/package.xml [new file with mode: 0644]
framework/File_CSV/CSV.php [new file with mode: 0644]
framework/File_CSV/package.xml [new file with mode: 0644]
framework/File_CSV/tests/001.csv [new file with mode: 0755]
framework/File_CSV/tests/001.phpt [new file with mode: 0755]
framework/File_CSV/tests/002.csv [new file with mode: 0755]
framework/File_CSV/tests/002.phpt [new file with mode: 0755]
framework/File_CSV/tests/003.csv [new file with mode: 0644]
framework/File_CSV/tests/003.phpt [new file with mode: 0644]
framework/File_CSV/tests/004.csv [new file with mode: 0644]
framework/File_CSV/tests/004.phpt [new file with mode: 0644]
framework/File_CSV/tests/005.csv [new file with mode: 0644]
framework/File_CSV/tests/005.phpt [new file with mode: 0644]
framework/File_CSV/tests/bug_3839.csv [new file with mode: 0644]
framework/File_CSV/tests/bug_3839.phpt [new file with mode: 0644]
framework/File_CSV/tests/bug_4025.csv [new file with mode: 0644]
framework/File_CSV/tests/bug_4025.phpt [new file with mode: 0644]
framework/File_CSV/tests/bug_6311.csv [new file with mode: 0644]
framework/File_CSV/tests/bug_6311.phpt [new file with mode: 0644]
framework/File_CSV/tests/bug_6370.csv [new file with mode: 0644]
framework/File_CSV/tests/bug_6370.phpt [new file with mode: 0644]
framework/File_CSV/tests/bug_6372.csv [new file with mode: 0644]
framework/File_CSV/tests/bug_6372.phpt [new file with mode: 0644]
framework/File_CSV/tests/columns.phpt [new file with mode: 0644]
framework/File_CSV/tests/columns1.csv [new file with mode: 0644]
framework/File_CSV/tests/columns2.csv [new file with mode: 0644]
framework/File_CSV/tests/common.php [new file with mode: 0644]
framework/File_CSV/tests/linebreak.phpt [new file with mode: 0644]
framework/File_CSV/tests/multiline.phpt [new file with mode: 0644]
framework/File_CSV/tests/multiline1.csv [new file with mode: 0644]
framework/File_CSV/tests/notrailing_crlf.csv [new file with mode: 0644]
framework/File_CSV/tests/notrailing_lf.csv [new file with mode: 0644]
framework/File_CSV/tests/quote1.csv [new file with mode: 0644]
framework/File_CSV/tests/quote2.csv [new file with mode: 0644]
framework/File_CSV/tests/quote3.csv [new file with mode: 0644]
framework/File_CSV/tests/quote4.csv [new file with mode: 0644]
framework/File_CSV/tests/quote5.csv [new file with mode: 0644]
framework/File_CSV/tests/quotes.phpt [new file with mode: 0644]
framework/File_CSV/tests/simple_cr.csv [new file with mode: 0644]
framework/File_CSV/tests/simple_crlf.csv [new file with mode: 0644]
framework/File_CSV/tests/simple_lf.csv [new file with mode: 0644]
framework/File_PDF/PDF.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/courier.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/helvetica.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/helveticab.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/helveticabi.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/helveticai.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/symbol.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/times.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/timesb.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/timesbi.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/timesi.php [new file with mode: 0644]
framework/File_PDF/PDF/fonts/zapfdingbats.php [new file with mode: 0644]
framework/File_PDF/package.xml [new file with mode: 0644]
framework/File_PDF/tests/20k_c1.txt [new file with mode: 0644]
framework/File_PDF/tests/20k_c2.txt [new file with mode: 0644]
framework/File_PDF/tests/auto_break.phpt [new file with mode: 0644]
framework/File_PDF/tests/factory.phpt [new file with mode: 0644]
framework/File_PDF/tests/hello_world.phpt [new file with mode: 0644]
framework/File_PDF/tests/horde-power1.png [new file with mode: 0644]
framework/File_PDF/tests/links.phpt [new file with mode: 0644]
framework/File_PDF/tests/locale_floats.phpt [new file with mode: 0644]
framework/File_PDF/tests/pear12310.phpt [new file with mode: 0644]
framework/File_PDF/tests/text_color.phpt [new file with mode: 0644]
framework/File_PDF/tests/underline.phpt [new file with mode: 0644]
framework/Form/Form.php [new file with mode: 0644]
framework/Form/Form/Action.php [new file with mode: 0644]
framework/Form/Form/Action/ConditionalEnable.php [new file with mode: 0644]
framework/Form/Form/Action/ConditionalSetValue.php [new file with mode: 0644]
framework/Form/Form/Action/conditional_enable.php [new file with mode: 0644]
framework/Form/Form/Action/conditional_setvalue.php [new file with mode: 0644]
framework/Form/Form/Action/reload.php [new file with mode: 0644]
framework/Form/Form/Action/setcursorpos.php [new file with mode: 0644]
framework/Form/Form/Action/submit.php [new file with mode: 0644]
framework/Form/Form/Action/sum_fields.php [new file with mode: 0644]
framework/Form/Form/Action/updatefield.php [new file with mode: 0644]
framework/Form/Form/Renderer.php [new file with mode: 0644]
framework/Form/Form/Type.php [new file with mode: 0644]
framework/Form/Form/Type/tableset.php [new file with mode: 0644]
framework/Form/Form/Variable.php [new file with mode: 0644]
framework/Form/package.xml [new file with mode: 0644]
framework/Form/tests/Horde_Form_Type_address.phpt [new file with mode: 0644]
framework/Form/tests/Horde_Form_Type_email.phpt [new file with mode: 0644]
framework/Group/Group.php [new file with mode: 0644]
framework/Group/Group/contactlists.php [new file with mode: 0644]
framework/Group/Group/hooks.php [new file with mode: 0644]
framework/Group/Group/kolab.php [new file with mode: 0644]
framework/Group/Group/ldap.php [new file with mode: 0644]
framework/Group/Group/mock.php [new file with mode: 0644]
framework/Group/Group/sql.php [new file with mode: 0644]
framework/Group/package.xml [new file with mode: 0644]
framework/Kolab/Kolab.php [new file with mode: 0644]
framework/Kolab/package.xml [new file with mode: 0644]
framework/Kolab_Filter/COPYING [new file with mode: 0644]
framework/Kolab_Filter/TODO [new file with mode: 0644]
framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.1 [new file with mode: 0644]
framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.pod [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Base.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Exception.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Incoming.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Outlook.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Response.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/DovecotLDA.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/LMTPTLS.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lda.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lmtp.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/smtp.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Resource.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Kolab.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Mock.php [new file with mode: 0644]
framework/Kolab_Filter/lib/Horde/Kolab/Test/Filter.php [new file with mode: 0644]
framework/Kolab_Filter/locale/de_DE/LC_MESSAGES/Kolab_Filter.mo [new file with mode: 0644]
framework/Kolab_Filter/locale/fr_FR/LC_MESSAGES/Kolab_Filter.mo [new file with mode: 0644]
framework/Kolab_Filter/locale/nl_NL/LC_MESSAGES/Kolab_Filter.mo [new file with mode: 0644]
framework/Kolab_Filter/package.xml [new file with mode: 0644]
framework/Kolab_Filter/po/Kolab_Filter.pot [new file with mode: 0644]
framework/Kolab_Filter/po/de_DE.po [new file with mode: 0644]
framework/Kolab_Filter/po/fr_FR.po [new file with mode: 0644]
framework/Kolab_Filter/po/nl_NL.po [new file with mode: 0644]
framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabfilter.php [new file with mode: 0644]
framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabmailboxfilter.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/AllTests.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/ContentTest.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/FilterTest.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/IncomingTest.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/LoadTest.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/ResourceTest.php [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/attendee_status_invitation.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty2.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged_trans.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/null.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/privileged.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret2 [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation2.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple2.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple_out.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/test.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.ret [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.eml [new file with mode: 0644]
framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.ret [new file with mode: 0644]
framework/LDAP/LDAP.php [new file with mode: 0644]
framework/LDAP/package.xml [new file with mode: 0644]
framework/LDAP/tests/quoteDN.phpt [new file with mode: 0644]
framework/Lens/lib/Horde/Lens.php [new file with mode: 0644]
framework/Lens/lib/Horde/Lens/Interface.php [new file with mode: 0644]
framework/Lens/lib/Horde/Lens/Iterator.php [new file with mode: 0644]
framework/Lens/package.xml [new file with mode: 0644]
framework/Mobile/Mobile.php [new file with mode: 0644]
framework/Mobile/Mobile/Renderer.php [new file with mode: 0644]
framework/Mobile/Mobile/Renderer/html.php [new file with mode: 0644]
framework/Mobile/Mobile/Renderer/wml.php [new file with mode: 0644]
framework/Mobile/package.xml [new file with mode: 0644]
framework/Net_IMSP/IMSP.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Auth.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Auth/cram_md5.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Auth/imtest.php [new file with mode: 0755]
framework/Net_IMSP/IMSP/Auth/plaintext.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Book.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Options.php [new file with mode: 0644]
framework/Net_IMSP/IMSP/Utils.php [new file with mode: 0644]
framework/Net_IMSP/package.xml [new file with mode: 0644]
framework/Net_SMS/SMS.php [new file with mode: 0644]
framework/Net_SMS/SMS/clickatell_http.php [new file with mode: 0644]
framework/Net_SMS/SMS/generic_smpp.php [new file with mode: 0644]
framework/Net_SMS/SMS/generic_smtp.php [new file with mode: 0644]
framework/Net_SMS/SMS/sms2email_http.php [new file with mode: 0644]
framework/Net_SMS/SMS/textmagic_http.php [new file with mode: 0644]
framework/Net_SMS/SMS/vodafoneitaly_smtp.php [new file with mode: 0644]
framework/Net_SMS/SMS/win_http.php [new file with mode: 0644]
framework/Net_SMS/package.xml [new file with mode: 0644]
framework/Reflection/lib/Horde/Reflection.php [new file with mode: 0644]
framework/Reflection/lib/Horde/Reflection/Cli.php [new file with mode: 0644]
framework/Reflection/lib/Horde/Reflection/Html.php [new file with mode: 0644]
framework/Reflection/lib/Horde/Reflection/Wiki.php [new file with mode: 0644]
framework/Reflection/package.xml [new file with mode: 0644]
framework/SQL/SQL.php [new file with mode: 0644]
framework/SQL/SQL/Keywords.php [new file with mode: 0644]
framework/SQL/SQL/Manager.php [new file with mode: 0644]
framework/SQL/package.xml [new file with mode: 0644]
framework/Scheduler/lib/Horde/Scheduler.php [new file with mode: 0644]
framework/Scheduler/lib/Horde/Scheduler/cron.php [new file with mode: 0644]
framework/Scheduler/package.xml [new file with mode: 0644]
framework/Scheduler/scripts/Horde/Scheduler/horde-crond.php [new file with mode: 0755]
framework/Share/Share.php [new file with mode: 0644]
framework/Share/Share/datatree.php [new file with mode: 0644]
framework/Share/Share/kolab.php [new file with mode: 0644]
framework/Share/Share/sql.php [new file with mode: 0644]
framework/Share/Share/sql_hierarchical.php [new file with mode: 0644]
framework/Share/package.xml [new file with mode: 0644]
framework/Share/tests/Horde/Share/AllTests.php [new file with mode: 0644]
framework/Share/tests/Horde/Share/KolabScenarioTest.php [new file with mode: 0644]
framework/Share/tests/kolab_createdefault.phpt [new file with mode: 0644]
framework/Share/tests/kolab_list.phpt [new file with mode: 0644]
framework/Share/tests/kolab_simple.phpt [new file with mode: 0644]
framework/SyncML/SyncML.php [new file with mode: 0644]
framework/SyncML/SyncML/Backend.php [new file with mode: 0644]
framework/SyncML/SyncML/Backend/Horde.php [new file with mode: 0644]
framework/SyncML/SyncML/Backend/Sql.php [new file with mode: 0644]
framework/SyncML/SyncML/Command.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Alert.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Final.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Get.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Map.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Put.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Replace.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Results.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Status.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/Sync.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/SyncElement.php [new file with mode: 0644]
framework/SyncML/SyncML/Command/SyncHdr.php [new file with mode: 0644]
framework/SyncML/SyncML/Constants.php [new file with mode: 0644]
framework/SyncML/SyncML/Device.php [new file with mode: 0644]
framework/SyncML/SyncML/Device/Nokia.php [new file with mode: 0644]
framework/SyncML/SyncML/Device/P800.php [new file with mode: 0644]
framework/SyncML/SyncML/Device/Sync4JMozilla.php [new file with mode: 0755]
framework/SyncML/SyncML/Device/Sync4j.php [new file with mode: 0644]
framework/SyncML/SyncML/Device/Synthesis.php [new file with mode: 0644]
framework/SyncML/SyncML/DeviceInfo.php [new file with mode: 0644]
framework/SyncML/SyncML/State.php [new file with mode: 0644]
framework/SyncML/SyncML/Sync.php [new file with mode: 0644]
framework/SyncML/SyncML/XMLOutput.php [new file with mode: 0644]
framework/SyncML/docs/INSTALL.p900.txt [new file with mode: 0644]
framework/SyncML/docs/README.program_flow.txt [new file with mode: 0644]
framework/SyncML/docs/README.syncml_primer.txt [new file with mode: 0644]
framework/SyncML/docs/TODO [new file with mode: 0644]
framework/SyncML/package.xml [new file with mode: 0644]
framework/SyncML/tests/sif.phpt [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/data.txt [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/devinf.txt [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_50.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_51.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_52.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_60.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_61.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_62.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_log.txt [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_50.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_51.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_52.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_60.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_61.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_62.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/data.txt [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_50.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_51.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_52.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_53.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_log.txt [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_50.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_51.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_52.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_53.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_13.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_23.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_33.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_43.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_42.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_10.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_11.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_12.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_20.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_21.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_22.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_30.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_31.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_32.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_40.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_41.xml [new file with mode: 0644]
framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_42.xml [new file with mode: 0644]
framework/SyncML/tests/testpacket.php [new file with mode: 0755]
framework/SyncML/tests/testsync.php [new file with mode: 0755]
framework/Text_Diff/Diff.php [new file with mode: 0644]
framework/Text_Diff/Diff/Engine/native.php [new file with mode: 0644]
framework/Text_Diff/Diff/Engine/shell.php [new file with mode: 0644]
framework/Text_Diff/Diff/Engine/string.php [new file with mode: 0644]
framework/Text_Diff/Diff/Engine/xdiff.php [new file with mode: 0644]
framework/Text_Diff/Diff/Mapped.php [new file with mode: 0644]
framework/Text_Diff/Diff/Renderer.php [new file with mode: 0644]
framework/Text_Diff/Diff/Renderer/context.php [new file with mode: 0644]
framework/Text_Diff/Diff/Renderer/inline.php [new file with mode: 0644]
framework/Text_Diff/Diff/Renderer/unified.php [new file with mode: 0644]
framework/Text_Diff/Diff/ThreeWay.php [new file with mode: 0644]
framework/Text_Diff/docs/examples/1.txt [new file with mode: 0644]
framework/Text_Diff/docs/examples/2.txt [new file with mode: 0644]
framework/Text_Diff/docs/examples/diff.php [new file with mode: 0644]
framework/Text_Diff/package.xml [new file with mode: 0644]
framework/Text_Diff/tests/1.txt [new file with mode: 0644]
framework/Text_Diff/tests/2.txt [new file with mode: 0644]
framework/Text_Diff/tests/3.txt [new file with mode: 0644]
framework/Text_Diff/tests/4.txt [new file with mode: 0644]
framework/Text_Diff/tests/5.txt [new file with mode: 0644]
framework/Text_Diff/tests/6.txt [new file with mode: 0644]
framework/Text_Diff/tests/context.patch [new file with mode: 0644]
framework/Text_Diff/tests/context.phpt [new file with mode: 0644]
framework/Text_Diff/tests/context2.phpt [new file with mode: 0644]
framework/Text_Diff/tests/diff.phpt [new file with mode: 0644]
framework/Text_Diff/tests/diff_shell.phpt [new file with mode: 0644]
framework/Text_Diff/tests/inline.phpt [new file with mode: 0644]
framework/Text_Diff/tests/inline2.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug12740.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug4879.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug4982.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug6251.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug6428.phpt [new file with mode: 0644]
framework/Text_Diff/tests/pear_bug7839.phpt [new file with mode: 0644]
framework/Text_Diff/tests/string.phpt [new file with mode: 0644]
framework/Text_Diff/tests/unified.patch [new file with mode: 0644]
framework/Text_Diff/tests/unified.phpt [new file with mode: 0644]
framework/Text_Diff/tests/unified2.patch [new file with mode: 0644]
framework/Text_Diff/tests/unified2.phpt [new file with mode: 0644]
framework/Text_Diff/tests/xdiff.phpt [new file with mode: 0644]
framework/VFS/data/VFS/muvfs.sql [new file with mode: 0644]
framework/VFS/data/VFS/vfs.mssql.sql [new file with mode: 0644]
framework/VFS/data/VFS/vfs.oci8.sql [new file with mode: 0644]
framework/VFS/data/VFS/vfs.pgsql.sql [new file with mode: 0644]
framework/VFS/data/VFS/vfs.sql [new file with mode: 0644]
framework/VFS/lib/VFS.php [new file with mode: 0644]
framework/VFS/lib/VFS/Browser.php [new file with mode: 0644]
framework/VFS/lib/VFS/GC.php [new file with mode: 0644]
framework/VFS/lib/VFS/ListItem.php [new file with mode: 0644]
framework/VFS/lib/VFS/Object.php [new file with mode: 0644]
framework/VFS/lib/VFS/file.php [new file with mode: 0644]
framework/VFS/lib/VFS/ftp.php [new file with mode: 0644]
framework/VFS/lib/VFS/horde.php [new file with mode: 0644]
framework/VFS/lib/VFS/kolab.php [new file with mode: 0644]
framework/VFS/lib/VFS/musql.php [new file with mode: 0644]
framework/VFS/lib/VFS/smb.php [new file with mode: 0644]
framework/VFS/lib/VFS/sql.php [new file with mode: 0644]
framework/VFS/lib/VFS/sql_file.php [new file with mode: 0644]
framework/VFS/lib/VFS/ssh2.php [new file with mode: 0644]
framework/VFS/package.xml [new file with mode: 0644]
framework/VFS/scripts/VFS/vfs.php [new file with mode: 0644]
framework/VFS/test/VFS/AllTests.php [new file with mode: 0644]
framework/VFS/test/VFS/KolabTest.php [new file with mode: 0644]
framework/VFS/test/VFS/SmbTest.php [new file with mode: 0644]
framework/VFS/test/VFS/fixtures/samba1.txt [new file with mode: 0755]
framework/VFS/test/VFS/fixtures/samba2.txt [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealInputStrategy.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/copy.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/direct.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/copy.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/direct.php [new file with mode: 0644]
framework/VFS_ISOWriter/ISOWriter/mkisofs.php [new file with mode: 0644]
framework/VFS_ISOWriter/package.xml [new file with mode: 0644]
framework/VFS_ISOWriter/tests/inputstrategy.phpt [new file with mode: 0644]
framework/VFS_ISOWriter/tests/isowriter.phpt [new file with mode: 0644]
framework/VFS_ISOWriter/tests/outputstrategy.phpt [new file with mode: 0644]
framework/XML_WBXML/WBXML.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/ContentHandler.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/DTD.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/DTD/SyncML.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/DTD/SyncMLDevInf.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/DTD/SyncMLMetInf.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/DTDManager.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/Decoder.php [new file with mode: 0644]
framework/XML_WBXML/WBXML/Encoder.php [new file with mode: 0644]
framework/XML_WBXML/docs/examples/MotorolaA780.wbxml [new file with mode: 0755]
framework/XML_WBXML/docs/examples/devinf.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/k500i_client_0.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/k700i_syncml_client_0.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_client_packet_1.xml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_client_packet_2.xml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_client_packet_3.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_client_packet_4.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_client_packet_5.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_server_packet_1.wbxml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/syncml_server_packet_1.xml [new file with mode: 0644]
framework/XML_WBXML/docs/examples/z600_client_0.wbxml [new file with mode: 0644]
framework/XML_WBXML/package.xml [new file with mode: 0644]
framework/XML_WBXML/tests/decode.php [new file with mode: 0755]

diff --git a/framework/Alarm/Alarm.php b/framework/Alarm/Alarm.php
new file mode 100644 (file)
index 0000000..35410e0
--- /dev/null
@@ -0,0 +1,580 @@
+<?php
+/**
+ * @package Horde_Alarm
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Alarm/Alarm.php,v 1.62 2009-11-23 20:37:47 slusarz Exp $
+ */
+
+/**
+ * The Horde_Alarm:: class provides an interface to deal with reminders,
+ * alarms and notifications through a standardized API.
+ *
+ * Alarm hashes have the following fields:
+ * - id: Unique alarm id.
+ * - user: The alarm's user. Empty if a global alarm.
+ * - start: The alarm start as a Horde_Date.
+ * - end: The alarm end as a Horde_Date.
+ * - methods: The notification methods for this alarm.
+ * - params: The paramters for the notification methods.
+ * - title: The alarm title.
+ * - text: An optional alarm description.
+ * - snooze: The snooze time (next time) of the alarm as a Horde_Date.
+ * - internal: Holds internally used data.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Alarm
+ */
+class Horde_Alarm {
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array('ttl' => 300);
+
+    /**
+     * An error message to throw when something is wrong.
+     *
+     * @var string
+     */
+    var $_errormsg;
+
+    /**
+     * Constructor - just store the $params in our newly-created object. All
+     * other work is done by initialize().
+     *
+     * @param array $params  Any parameters needed for this driver.
+     */
+    function Horde_Alarm($params = array(), $errormsg = null)
+    {
+        $this->_params = array_merge($this->_params, $params);
+        if ($errormsg === null) {
+            $this->_errormsg = _("The alarm backend is not currently available.");
+        } else {
+            $this->_errormsg = $errormsg;
+        }
+    }
+
+    /**
+     * Returns an alarm hash from the backend.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user
+     *
+     * @return array  An alarm hash.
+     */
+    function get($id, $user)
+    {
+        $alarm = $this->_get($id, $user);
+        if (is_a($alarm, 'PEAR_Error')) {
+            return $alarm;
+        }
+        if (isset($alarm['mail']['body'])) {
+            $alarm['mail']['body'] = $this->_fromDriver($alarm['mail']['body']);
+        }
+        return $alarm;
+    }
+
+    /**
+     * Stores an alarm hash in the backend.
+     *
+     * The alarm will be added if it doesn't exist, and updated otherwise.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function set($alarm)
+    {
+        if (isset($alarm['mail']['body'])) {
+            $alarm['mail']['body'] = $this->_toDriver($alarm['mail']['body']);
+        }
+        if ($this->exists($alarm['id'], isset($alarm['user']) ? $alarm['user'] : '')) {
+            return $this->_update($alarm);
+        } else {
+            return $this->_add($alarm);
+        }
+    }
+
+    /**
+     * Returns whether an alarm with the given id exists already.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user
+     *
+     * @return boolean  True if the specified alarm exists.
+     */
+    function exists($id, $user)
+    {
+        $exists = $this->_exists($id, $user);
+        return $exists && !is_a($exists, 'PEAR_Error');
+    }
+
+    /**
+     * Delays (snoozes) an alarm for a certain period.
+     *
+     * @param string $id        The alarm's unique id.
+     * @param string $user      The notified user.
+     * @param integer $minutes  The delay in minutes. A negative value
+     *                          dismisses the alarm completely.
+     */
+    function snooze($id, $user, $minutes)
+    {
+        $alarm = $this->get($id, $user);
+        if (is_a($alarm, 'PEAR_Error')) {
+            return $alarm;
+        }
+        if (empty($user)) {
+            return PEAR::raiseError(_("This alarm cannot be snoozed."));
+        }
+        if ($alarm) {
+            if ($minutes > 0) {
+                $alarm['snooze'] = new Horde_Date(time());
+                $alarm['snooze']->min += $minutes;
+                return $this->_snooze($id, $user, $alarm['snooze']);
+            } else {
+                return $this->_dismiss($id, $user);
+            }
+        }
+    }
+
+    /**
+     * Returns whether an alarm is snoozed.
+     *
+     * @param string $id        The alarm's unique id.
+     * @param string $user      The alarm's user
+     * @param Horde_Date $time  The time when the alarm may be snoozed.
+     *                          Defaults to now.
+     *
+     * @return boolean  True if the alarm is snoozed.
+     */
+    function isSnoozed($id, $user, $time = null)
+    {
+        if (is_null($time)) {
+            $time = new Horde_Date(time());
+        }
+        return (bool)$this->_isSnoozed($id, $user, $time);
+    }
+
+    /**
+     * Deletes an alarm from the backend.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user. All users' alarms if null.
+     */
+    function delete($id, $user = null)
+    {
+        return $this->_delete($id, $user);
+    }
+
+    /**
+     * Retrieves active alarms from all applications and stores them in the
+     * backend.
+     *
+     * The applications will only be called once in the configured time span,
+     * by default 5 minutes.
+     *
+     * @param string $user      Retrieve alarms for this user, or for all users
+     *                          if null.
+     * @param boolean $preload  Preload alarms that go off within the next
+     *                          ttl time span?
+     */
+    function load($user = null, $preload = true)
+    {
+        if (isset($_SESSION['horde']['alarm']['loaded']) &&
+            time() - $_SESSION['horde']['alarm']['loaded'] < $this->_params['ttl']) {
+            return;
+        }
+
+        $apps = $GLOBALS['registry']->listApps(null, false, Horde_Perms::READ);
+        if (is_a($apps, 'PEAR_Error')) {
+            return false;
+        }
+        foreach ($apps as $app) {
+            if ($GLOBALS['registry']->hasMethod('listAlarms', $app)) {
+                try {
+                    $pushed = $GLOBALS['registry']->pushApp($app, array('check_perms' => false));
+                } catch (Horde_Exception $e) {
+                    Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    continue;
+                }
+                /* Preload alarms that happen in the next ttl seconds. */
+                if ($preload) {
+                    try {
+                        $alarms = $GLOBALS['registry']->callByPackage($app, 'listAlarms', array(time() + $this->_params['ttl'], $user));
+                    } catch (Horde_Exception $e) {
+                        if ($pushed) {
+                            $GLOBALS['registry']->popApp();
+                        }
+                        continue;
+                    }
+                } else {
+                    $alarms = array();
+                }
+
+                /* Load current alarms if no preloading requested or if this
+                 * is the first call in this session. */
+                if (!$preload || !isset($_SESSION['horde']['alarm']['loaded'])) {
+                    try {
+                        $app_alarms = $GLOBALS['registry']->callByPackage($app, 'listAlarms', array(time(), $user));
+                    } catch (Horde_Exception $e) {
+                        Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR);
+                        $app_alarms = array();
+                    }
+                    $alarms = array_merge($alarms, $app_alarms);
+                }
+
+                if ($pushed) {
+                    $GLOBALS['registry']->popApp();
+                }
+
+                if (empty($alarms)) {
+                    continue;
+                }
+
+                foreach ($alarms as $alarm) {
+                    $alarm['start'] = new Horde_Date($alarm['start']);
+                    if (!empty($alarm['end'])) {
+                        $alarm['end'] = new Horde_Date($alarm['end']);
+                    }
+                    $this->set($alarm);
+                }
+            }
+        }
+
+        $_SESSION['horde']['alarm']['loaded'] = time();
+    }
+
+    /**
+     * Returns a list of alarms from the backend.
+     *
+     * @param string $user      Return alarms for this user, all users if
+     *                          null, or global alarms if empty.
+     * @param Horde_Date $time  The time when the alarms should be active.
+     *                          Defaults to now.
+     * @param boolean $load     Update active alarms from all applications?
+     * @param boolean $preload  Preload alarms that go off within the next
+     *                          ttl time span?
+     *
+     * @return array  A list of alarm hashes.
+     */
+    function listAlarms($user = null, $time = null, $load = false,
+                        $preload = true)
+    {
+        if (empty($time)) {
+            $time = new Horde_Date(time());
+        }
+        if ($load) {
+            $this->load($user, $preload);
+        }
+
+        $alarms = $this->_list($user, $time);
+        if (is_a($alarms, 'PEAR_Error')) {
+            return $alarms;
+        }
+
+        foreach (array_keys($alarms) as $alarm) {
+            if (isset($alarms[$alarm]['mail']['body'])) {
+                $alarms[$alarm]['mail']['body'] = $this->_fromDriver($alarms[$alarm]['mail']['body']);
+            }
+        }
+        return $alarms;
+    }
+
+    /**
+     * Notifies the user about any active alarms.
+     *
+     * @param string $user      Notify this user, all users if null, or guest
+     *                          users if empty.
+     * @param boolean $load     Update active alarms from all applications?
+     * @param boolean $preload  Preload alarms that go off within the next
+     *                          ttl time span?
+     * @param array $exclude    Don't notify with these methods.
+     */
+    function notify($user = null, $load = true, $preload = true,
+                    $exclude = array())
+    {
+        $alarms = $this->listAlarms($user, null, $load, $preload);
+        if (is_a($alarms, 'PEAR_Error')) {
+            Horde::logMessage($alarms, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $alarms;
+        }
+        if (empty($alarms)) {
+            return;
+        }
+
+        $methods = array_keys($this->notificationMethods());
+        foreach ($alarms as $alarm) {
+            foreach ($alarm['methods'] as $alarm_method) {
+                if (in_array($alarm_method, $methods) &&
+                    !in_array($alarm_method, $exclude)) {
+                    $result = $this->{'_' . $alarm_method}($alarm);
+                    if (is_a($result, 'PEAR_Error')) {
+                        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Notifies about an alarm through Horde_Notification.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function _notify($alarm)
+    {
+        static $sound_played;
+
+        $GLOBALS['notification']->push($alarm['title'], 'horde.alarm', array('alarm' => $alarm));
+        if (!empty($alarm['params']['notify']['sound']) &&
+            !isset($sound_played[$alarm['params']['notify']['sound']])) {
+            require_once 'Horde/Notification/Listener/Audio.php';
+            $GLOBALS['notification']->attach('audio');
+            $GLOBALS['notification']->push($alarm['params']['notify']['sound'], 'audio');
+            $sound_played[$alarm['params']['notify']['sound']] = true;
+        }
+    }
+
+    /**
+     * Notifies about an alarm by email.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function _mail($alarm)
+    {
+        if (!empty($alarm['internal']['mail']['sent'])) {
+            return;
+        }
+
+        if (empty($alarm['params']['mail']['email'])) {
+            if (empty($alarm['user'])) {
+                return;
+            }
+            $identity = Horde_Prefs_Identity::singleton('none', $alarm['user']);
+            $email = $identity->getDefaultFromAddress(true);
+        } else {
+            $email = $alarm['params']['mail']['email'];
+        }
+
+        $mail = new Horde_Mime_Mail(array(
+            'subject' => $alarm['title'],
+            'body' => empty($alarm['params']['mail']['body']) ? $alarm['text'] : $alarm['params']['mail']['body'],
+            'to' => $email,
+            'from' => $email,
+            'charset' => Horde_Nls::getCharset()));
+        $mail->addHeader('Auto-Submitted', 'auto-generated');
+        $mail->addHeader('X-Horde-Alarm', $alarm['title'], Horde_Nls::getCharset());
+        $sent = $mail->send(Horde::getMailerConfig());
+        if (is_a($sent, 'PEAR_Error')) {
+            return $sent;
+        }
+
+        $alarm['internal']['mail']['sent'] = true;
+        $this->_internal($alarm['id'], $alarm['user'], $alarm['internal']);
+    }
+
+    /**
+     * Notifies about an alarm with an SMS through the sms/send API method.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function _sms($alarm)
+    {
+    }
+
+    /**
+     * Returns a list of available notification methods and method parameters.
+     *
+     * The returned list is a hash with method names as the keys and
+     * optionally associated parameters as values. The parameters are hashes
+     * again with parameter names as keys and parameter information as
+     * values. The parameter information is hash with the following keys:
+     * 'desc' contains a parameter description; 'required' specifies whether
+     * this parameter is required.
+     *
+     * @return array  List of methods and parameters.
+     */
+    function notificationMethods()
+    {
+        static $methods;
+
+        if (!isset($methods)) {
+            $methods = array('notify' => array(
+                                 '__desc' => _("Inline Notification"),
+                                 'sound' => array('type' => 'sound',
+                                                  'desc' => _("Play a sound?"),
+                                                  'required' => false)),
+                             'mail' => array(
+                                 '__desc' => _("Email Notification"),
+                                 'email' => array('type' => 'text',
+                                                  'desc' => _("Email address (optional)"),
+                                                  'required' => false)));
+            /*
+            if ($GLOBALS['registry']->hasMethod('sms/send')) {
+                $methods['sms'] = array(
+                    'phone' => array('type' => 'text',
+                                     'desc' => _("Cell phone number"),
+                                     'required' => true));
+            }
+            */
+        }
+
+        return $methods;
+    }
+
+    /**
+     * Garbage collects old alarms in the backend.
+     */
+    function gc()
+    {
+        /* A 1% chance we will run garbage collection during a call. */
+        if (rand(0, 99) != 0) {
+            return;
+        }
+
+        return $this->_gc();
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Alarm instance based on $driver.
+     *
+     * @param string $driver  The type of concrete Horde_Alarm subclass to
+     *                        return. The class name is based on the storage
+     *                        driver ($driver). The code is dynamically
+     *                        included.
+     * @param array $params   A hash containing any additional configuration
+     *                        or connection parameters a subclass might need.
+     *
+     * @return mixed  The newly created concrete Horde_Alarm instance, or false
+     *                on an error.
+     */
+    static function factory($driver = null, $params = null)
+    {
+        if (is_null($driver)) {
+            $driver = empty($GLOBALS['conf']['alarms']['driver']) ? 'sql' : $GLOBALS['conf']['alarms']['driver'];
+        }
+
+        $driver = basename($driver);
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('alarms', $driver);
+        }
+
+        $class = 'Horde_Alarm_' . $driver;
+        if (class_exists($class)) {
+            $alarm = new $class($params);
+            $result = $alarm->initialize();
+            if (is_a($result, 'PEAR_Error')) {
+                $alarm = new Horde_Alarm($params, sprintf(_("The alarm backend is not currently available: %s"), $result->getMessage()));
+            } else {
+                $alarm->gc();
+            }
+        } else {
+            $alarm = new Horde_Alarm($params, sprintf(_("Unable to load the definition of %s."), $class));
+        }
+
+        return $alarm;
+    }
+
+    /**
+     * Converts a value from the driver's charset.
+     *
+     * @param mixed $value  Value to convert.
+     *
+     * @return mixed  Converted value.
+     */
+    function _fromDriver($value)
+    {
+        return $value;
+    }
+
+    /**
+     * Converts a value to the driver's charset.
+     *
+     * @param mixed $value  Value to convert.
+     *
+     * @return mixed  Converted value.
+     */
+    function _toDriver($value)
+    {
+        return $value;
+    }
+
+    /**
+     * @abstract
+     */
+    function _get()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _list()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _add()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _update()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _internal()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _exists()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _snooze()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _isSnoozed()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+    /**
+     * @abstract
+     */
+    function _delete()
+    {
+        return PEAR::raiseError($this->_errormsg);
+    }
+
+}
diff --git a/framework/Alarm/Alarm/sql.php b/framework/Alarm/Alarm/sql.php
new file mode 100644 (file)
index 0000000..2581e6a
--- /dev/null
@@ -0,0 +1,477 @@
+<?php
+/**
+ * @package Horde_Alarm
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Alarm/Alarm/sql.php,v 1.22 2009-12-10 14:06:27 jan Exp $
+ */
+
+/**
+ * The Horde_Alarm_sql:: class is a Horde_Alarm storage implementation using
+ * the PEAR DB package.
+ *
+ * Required values for $params:<pre>
+ *      'phptype'       The database type (e.g. 'pgsql', 'mysql', etc.).
+ *      'charset'       The database's internal charset.</pre>
+ *
+ * Optional values for $params:<pre>
+ *      'table'         The name of the foo table in 'database'.
+ *
+ * Required by some database implementations:<pre>
+ *      'database'      The name of the database.
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.</pre>
+ *
+ * The table structure can be created by the scripts/sql/horde_alarm.sql
+ * script.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.2
+ * @package Horde_Alarm
+ */
+class Horde_Alarm_sql extends Horde_Alarm {
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $_db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * Constructs a new SQL storage object.
+     *
+     * @param array $params  A hash containing connection parameters.
+     */
+    function Horde_Alarm_sql($params = array())
+    {
+        $this->_params = array_merge($this->_params, $params);
+    }
+
+    /**
+     * Converts a value from the driver's charset.
+     *
+     * @param mixed $value  Value to convert.
+     *
+     * @return mixed  Converted value.
+     */
+    function _fromDriver($value)
+    {
+        return Horde_String::convertCharset($value, $this->_params['charset']);
+    }
+
+    /**
+     * Converts a value to the driver's charset.
+     *
+     * @param mixed $value  Value to convert.
+     *
+     * @return mixed  Converted value.
+     */
+    function _toDriver($value)
+    {
+        return Horde_String::convertCharset($value, Horde_Nls::getCharset(), $this->_params['charset']);
+    }
+
+    /**
+     * Returns an alarm hash from the backend.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user
+     *
+     * @return array  An alarm hash.
+     */
+    function _get($id, $user)
+    {
+        $query = sprintf('SELECT alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze, alarm_internal FROM %s WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_get(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $alarm = $this->_db->getRow($query, array($id, $user), DB_FETCHMODE_ASSOC);
+        if (is_a($alarm, 'PEAR_Error')) {
+            Horde::logMessage($alarm, __FILE__, __LINE__);
+            return $alarm;
+        }
+        if (empty($alarm)) {
+            return PEAR::raiseError(_("Alarm not found"));
+        }
+        $alarm = array(
+            'id' => $alarm['alarm_id'],
+            'user' => $alarm['alarm_uid'],
+            'start' => new Horde_Date($alarm['alarm_start']),
+            'end' => empty($alarm['alarm_end']) ? null : new Horde_Date($alarm['alarm_end']),
+            'methods' => @unserialize($alarm['alarm_methods']),
+            'params' => @unserialize($alarm['alarm_params']),
+            'title' => $this->_fromDriver($alarm['alarm_title']),
+            'text' => $this->_fromDriver($alarm['alarm_text']),
+            'snooze' => empty($alarm['alarm_snooze']) ? null : new Horde_Date($alarm['alarm_snooze']),
+            'internal' => empty($alarm['alarm_internal']) ? null : @unserialize($alarm['alarm_internal']));
+        return $alarm;
+    }
+
+    /**
+     * Returns a list of alarms from the backend.
+     *
+     * @param Horde_Date $time  The time when the alarms should be active.
+     * @param string $user      Return alarms for this user, all users if
+     *                          null, or global alarms if empty.
+     *
+     * @return array  A list of alarm hashes.
+     */
+    function _list($user, $time)
+    {
+        $query = sprintf('SELECT alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze, alarm_internal FROM %s WHERE alarm_dismissed = 0 AND ((alarm_snooze IS NULL AND alarm_start <= ?) OR alarm_snooze <= ?) AND (alarm_end IS NULL OR alarm_end >= ?)%s ORDER BY alarm_start, alarm_end',
+                         $this->_params['table'],
+                         is_null($user) ? '' : ' AND (alarm_uid IS NULL OR alarm_uid = ? OR alarm_uid = ?)');
+        $dt = $time->format('Y-m-d\TH:i:s');
+        $values = array($dt, $dt, $dt);
+        if (!is_null($user)) {
+            $values[] = '';
+            $values[] = (string)$user;
+        }
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_list(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $alarms = array();
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+            return $result;
+        }
+        while ($alarm = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
+            if (is_a($alarm, 'PEAR_Error')) {
+                Horde::logMessage($alarm, __FILE__, __LINE__);
+                return $alarm;
+            }
+            $alarms[$alarm['alarm_id']] = array(
+                'id' => $alarm['alarm_id'],
+                'user' => $alarm['alarm_uid'],
+                'start' => new Horde_Date($alarm['alarm_start']),
+                'end' => empty($alarm['alarm_end']) ? null : new Horde_Date($alarm['alarm_end']),
+                'methods' => @unserialize($alarm['alarm_methods']),
+                'params' => @unserialize($alarm['alarm_params']),
+                'title' => $this->_fromDriver($alarm['alarm_title']),
+                'text' => $this->_fromDriver($alarm['alarm_text']),
+                'snooze' => empty($alarm['alarm_snooze']) ? null : new Horde_Date($alarm['alarm_snooze']),
+                'internal' => empty($alarm['alarm_internal']) ? null : @unserialize($alarm['alarm_internal']));
+        }
+
+        return $alarms;
+    }
+
+    /**
+     * Adds an alarm hash to the backend.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function _add($alarm)
+    {
+        $query = sprintf('INSERT INTO %s (alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', $this->_params['table']);
+        $values = array($alarm['id'],
+                        isset($alarm['user']) ? $alarm['user'] : '',
+                        (string)$alarm['start'],
+                        empty($alarm['end']) ? null : (string)$alarm['end'],
+                        serialize($alarm['methods']),
+                        serialize($alarm['params']),
+                        $this->_toDriver($alarm['title']),
+                        empty($alarm['text']) ? null : $this->_toDriver($alarm['text']),
+                        null);
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_add(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Updates an alarm hash in the backend.
+     *
+     * @param array $alarm  An alarm hash.
+     */
+    function _update($alarm)
+    {
+        $query = sprintf('UPDATE %s set alarm_start = ?, alarm_end = ?, alarm_methods = ?, alarm_params = ?, alarm_title = ?, alarm_text = ? WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         isset($alarm['user']) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        $values = array((string)$alarm['start'],
+                        empty($alarm['end']) ? null : (string)$alarm['end'],
+                        serialize($alarm['methods']),
+                        serialize($alarm['params']),
+                        $this->_toDriver($alarm['title']),
+                        empty($alarm['text'])
+                              ? null
+                              : $this->_toDriver($alarm['text']),
+                        $alarm['id'],
+                        isset($alarm['user']) ? $alarm['user'] : '');
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_update(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Updates internal alarm properties, i.e. properties not determined by
+     * the application setting the alarm.
+     *
+     * @param string $id       The alarm's unique id.
+     * @param string $user     The alarm's user
+     * @param array $internal  A hash with the internal data.
+     */
+    function _internal($id, $user, $internal)
+    {
+        $query = sprintf('UPDATE %s set alarm_internal = ? WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        $values = array(serialize($internal), $id, $user);
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_internal(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Returns whether an alarm with the given id exists already.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user
+     *
+     * @return boolean  True if the specified alarm exists.
+     */
+    function _exists($id, $user)
+    {
+        $query = sprintf('SELECT 1 FROM %s WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_exists(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getOne($query, array($id, $user));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Delays (snoozes) an alarm for a certain period.
+     *
+     * @param string $id          The alarm's unique id.
+     * @param string $user        The alarm's user
+     * @param Horde_Date $snooze  The snooze time.
+     */
+    function _snooze($id, $user, $snooze)
+    {
+        $query = sprintf('UPDATE %s set alarm_snooze = ? WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        $values = array((string)$snooze, $id, $user);
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_snooze(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Dismisses an alarm.
+     *
+     * @param string $id          The alarm's unique id.
+     * @param string $user        The alarm's user
+     */
+    function _dismiss($id, $user)
+    {
+        $query = sprintf('UPDATE %s set alarm_dismissed = 1 WHERE alarm_id = ? AND %s',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        $values = array($id, $user);
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_dismiss(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Returns whether an alarm is snoozed.
+     *
+     * @param string $id        The alarm's unique id.
+     * @param string $user      The alarm's user
+     * @param Horde_Date $time  The time when the alarm may be snoozed.
+     *
+     * @return boolean  True if the alarm is snoozed.
+     */
+    function _isSnoozed($id, $user, $time)
+    {
+        $query = sprintf('SELECT 1 FROM %s WHERE alarm_id = ? AND %s AND (alarm_dismissed = 1 OR (alarm_snooze IS NOT NULL AND alarm_snooze >= ?))',
+                         $this->_params['table'],
+                         !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)');
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_isSnoozed(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getOne($query, array($id, $user, (string)$time));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Deletes an alarm from the backend.
+     *
+     * @param string $id    The alarm's unique id.
+     * @param string $user  The alarm's user. All users' alarms if null.
+     */
+    function _delete($id, $user = null)
+    {
+        $query = sprintf('DELETE FROM %s WHERE alarm_id = ?', $this->_params['table']);
+        $values = array($id);
+        if (!is_null($user)) {
+            $query .= empty($user)
+                ? ' AND (alarm_uid IS NULL OR alarm_uid = ?)'
+                : ' AND alarm_uid = ?';
+            $values[] = $user;
+        }
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_delete(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Garbage collects old alarms in the backend.
+     */
+    function _gc()
+    {
+        $query = sprintf('DELETE FROM %s WHERE alarm_end IS NOT NULL AND alarm_end < ?', $this->_params['table']);
+        Horde::logMessage('SQL query by Horde_Alarm_sql::_gc(): ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $end = new Horde_Date(time());
+        $result = $this->_write_db->query($query, (string)$end);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__);
+        }
+        return $result;
+    }
+
+    /**
+     * Attempts to open a connection to the SQL server.
+     *
+     * @return boolean  True on success, PEAR_Error on failure.
+     */
+    function initialize()
+    {
+        Horde::assertDriverConfig($this->_params, 'sql',
+                                  array('phptype', 'charset'));
+
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+        if (!isset($this->_params['table'])) {
+            $this->_params['table'] = 'horde_alarms';
+        }
+
+        /* Connect to the SQL server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_write_db = &DB::connect($this->_params,
+                                        array('persistent' => !empty($this->_params['persistent']),
+                                              'ssl' => !empty($this->_params['ssl'])));
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            Horde::logMessage($this->_write_db, __FILE__, __LINE__);
+            return $this->_write_db;
+        }
+        $this->_initConn($this->_write_db);
+
+        /* Check if we need to set up the read DB connection seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = &DB::connect($params,
+                                      array('persistent' => !empty($params['persistent']),
+                                            'ssl' => !empty($params['ssl'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                Horde::logMessage($this->_db, __FILE__, __LINE__);
+                return $this->_db;
+            }
+            $this->_initConn($this->_db);
+        } else {
+            /* Default to the same DB handle for the writer too. */
+            $this->_db = &$this->_write_db;
+        }
+
+        return true;
+    }
+
+    /**
+     */
+    function _initConn(&$db)
+    {
+        // Set DB portability options.
+        switch ($db->phptype) {
+        case 'mssql':
+            $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+
+        default:
+            $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+        }
+
+        /* Handle any database specific initialization code to run. */
+        switch ($db->dbsyntax) {
+        case 'oci8':
+            $query = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'";
+
+            /* Log the query at a DEBUG log level. */
+            Horde::logMessage(sprintf('SQL connection setup for Alarms, query = "%s"', $query),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $db->query($query);
+            break;
+
+        case 'pgsql':
+            $query = "SET datestyle TO 'iso'";
+
+            /* Log the query at a DEBUG log level. */
+            Horde::logMessage(sprintf('SQL connection setup for Alarms, query = "%s"', $query),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $db->query($query);
+            break;
+        }
+    }
+
+}
diff --git a/framework/Alarm/package.xml b/framework/Alarm/package.xml
new file mode 100644 (file)
index 0000000..79c5968
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Alarm</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde alarm libraries</summary>
+ <description>This package provides an interface to deal with reminders,
+ alarms and notifications through a standardized API. The following
+ notification methods are available at the moment: standard Horde
+ notifications, popups, emails, sms.</description>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2007-02-01</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial release.</notes>
+ <contents>
+  <dir name="/" baseinstalldir="/Horde">
+   <dir name="Alarm">
+    <file name="sql.php" role="php" />
+   </dir> <!-- /Alarm -->
+   <dir name="tests">
+    <file name="001.phpt" role="test" />
+   </dir> <!-- /tests -->
+   <file name="Alarm.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Date</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease />
+</package>
diff --git a/framework/Alarm/tests/001.phpt b/framework/Alarm/tests/001.phpt
new file mode 100644 (file)
index 0000000..814c234
--- /dev/null
@@ -0,0 +1,487 @@
+--TEST--
+Horde_Alarm tests.
+--SKIPIF--
+<?php
+$setup = @include dirname(__FILE__) . '/setup.inc';
+if (!$setup || empty($params)) {
+    echo 'skip No SQL configuration provided.';
+}
+?>
+--FILE--
+<?php
+
+include dirname(__FILE__) . '/setup.inc';
+require dirname(__FILE__) . '/../Alarm.php';
+require 'Horde.php';
+require 'Horde/Nls.php';
+
+$alarm = Horde_Alarm::factory('sql', $params);
+
+$now = time();
+$date = new Horde_Date($now);
+$end = new Horde_Date($now + 3600);
+$hash = array('id' => 'personalalarm',
+              'user' => 'john',
+              'start' => $date,
+              'end' => $end,
+              'methods' => array(),
+              'params' => array(),
+              'title' => 'This is a personal alarm.');
+
+var_dump($alarm->set($hash));
+var_dump($alarm->exists('personalalarm', 'john'));
+$saved = $alarm->get('personalalarm', 'john');
+var_dump($saved);
+var_dump($saved['start']->compareDateTime($date));
+$hash['title'] = 'Changed alarm text';
+var_dump($alarm->set($hash));
+$date->min--;
+$alarm->set(array('id' => 'publicalarm',
+                  'start' => $date,
+                  'end' => $end,
+                  'methods' => array(),
+                  'params' => array(),
+                  'title' => 'This is a public alarm.'));
+var_dump($alarm->listAlarms('john'));
+var_dump($alarm->delete('publicalarm', ''));
+var_dump($alarm->listAlarms('john'));
+$error = $alarm->snooze('personalalarm', 'jane', 30);
+var_dump($error->getMessage());
+var_dump($alarm->snooze('personalalarm', 'john', 30));
+var_dump($alarm->isSnoozed('personalalarm', 'john'));
+var_dump($alarm->listAlarms('john'));
+var_dump($alarm->listAlarms('john', $end));
+var_dump($alarm->set(array('id' => 'noend',
+                           'user' => 'john',
+                           'start' => $date,
+                           'methods' => array('notify'),
+                           'params' => array(),
+                           'title' => 'This is an alarm without end.')));
+var_dump($alarm->listAlarms('john', $end));
+var_dump($alarm->delete('noend', 'john'));
+var_dump($alarm->delete('personalalarm', 'john'));
+
+?>
+--EXPECTF--
+int(1)
+bool(true)
+array(10) {
+  ["id"]=>
+  string(13) "personalalarm"
+  ["user"]=>
+  string(4) "john"
+  ["start"]=>
+  object(horde_date)(7) {
+    ["year"]=>
+    int(%d%d%d%d)
+    ["month"]=>
+    int(%d)
+    ["mday"]=>
+    int(%d)
+    ["hour"]=>
+    int(%d)
+    ["min"]=>
+    int(%d)
+    ["sec"]=>
+    int(%d)
+    ["_supportedSpecs"]=>
+    string(21) "%CdDeHImMnRStTyYbBpxX"
+  }
+  ["end"]=>
+  object(horde_date)(7) {
+    ["year"]=>
+    int(%d%d%d%d)
+    ["month"]=>
+    int(%d)
+    ["mday"]=>
+    int(%d)
+    ["hour"]=>
+    int(%d)
+    ["min"]=>
+    int(%d)
+    ["sec"]=>
+    int(%d)
+    ["_supportedSpecs"]=>
+    string(21) "%CdDeHImMnRStTyYbBpxX"
+  }
+  ["methods"]=>
+  array(0) {
+  }
+  ["params"]=>
+  array(0) {
+  }
+  ["title"]=>
+  string(25) "This is a personal alarm."
+  ["text"]=>
+  NULL
+  ["snooze"]=>
+  NULL
+  ["internal"]=>
+  NULL
+}
+int(0)
+int(1)
+array(2) {
+  ["publicalarm"]=>
+  array(10) {
+    ["id"]=>
+    string(11) "publicalarm"
+    ["user"]=>
+    string(0) ""
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["methods"]=>
+    array(0) {
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(23) "This is a public alarm."
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    NULL
+    ["internal"]=>
+    NULL
+  }
+  ["personalalarm"]=>
+  array(10) {
+    ["id"]=>
+    string(13) "personalalarm"
+    ["user"]=>
+    string(4) "john"
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["methods"]=>
+    array(0) {
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(18) "Changed alarm text"
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    NULL
+    ["internal"]=>
+    NULL
+  }
+}
+int(1)
+array(1) {
+  ["personalalarm"]=>
+  array(10) {
+    ["id"]=>
+    string(13) "personalalarm"
+    ["user"]=>
+    string(4) "john"
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["methods"]=>
+    array(0) {
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(18) "Changed alarm text"
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    NULL
+    ["internal"]=>
+    NULL
+  }
+}
+string(15) "Alarm not found"
+int(1)
+bool(true)
+array(0) {
+}
+array(1) {
+  ["personalalarm"]=>
+  array(10) {
+    ["id"]=>
+    string(13) "personalalarm"
+    ["user"]=>
+    string(4) "john"
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["methods"]=>
+    array(0) {
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(18) "Changed alarm text"
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["internal"]=>
+    NULL
+  }
+}
+int(1)
+array(2) {
+  ["noend"]=>
+  array(10) {
+    ["id"]=>
+    string(5) "noend"
+    ["user"]=>
+    string(4) "john"
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    NULL
+    ["methods"]=>
+    array(1) {
+      [0]=>
+      string(6) "notify"
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(29) "This is an alarm without end."
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    NULL
+    ["internal"]=>
+    NULL
+  }
+  ["personalalarm"]=>
+  array(10) {
+    ["id"]=>
+    string(13) "personalalarm"
+    ["user"]=>
+    string(4) "john"
+    ["start"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["end"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["methods"]=>
+    array(0) {
+    }
+    ["params"]=>
+    array(0) {
+    }
+    ["title"]=>
+    string(18) "Changed alarm text"
+    ["text"]=>
+    NULL
+    ["snooze"]=>
+    object(horde_date)(7) {
+      ["year"]=>
+      int(%d%d%d%d)
+      ["month"]=>
+      int(%d)
+      ["mday"]=>
+      int(%d)
+      ["hour"]=>
+      int(%d)
+      ["min"]=>
+      int(%d)
+      ["sec"]=>
+      int(%d)
+      ["_supportedSpecs"]=>
+      string(21) "%CdDeHImMnRStTyYbBpxX"
+    }
+    ["internal"]=>
+    NULL
+  }
+}
+int(1)
+int(1)
diff --git a/framework/Alarm/tests/setup.inc.dist b/framework/Alarm/tests/setup.inc.dist
new file mode 100644 (file)
index 0000000..996437c
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+$params = array('phptype' => 'mysql',
+                'database' => 'horde',
+                'username' => 'horde',
+                'password' => 'horde',
+                'charset' => 'iso-8859-1');
+
+require_once 'Log.php';
+$conf['log'] = array('priority' => PEAR_LOG_DEBUG,
+                     'ident' => 'HEADHORDE',
+                     'params' => array('append' => true),
+                     'name' => '/tmp/horde.log',
+                     'type' => 'file',
+                     'enabled' => true);
diff --git a/framework/DOM/DOM.php b/framework/DOM/DOM.php
new file mode 100644 (file)
index 0000000..e80fd20
--- /dev/null
@@ -0,0 +1,640 @@
+<?php
+/**
+ * The Horde_DOM_Document and related classes provide a PHP 4 domxml
+ * compatible wrapper around the PHP 5 dom implementation to allow scripts
+ * written for PHP 4 domxml model to work under PHP 5's dom support.
+ *
+ * This code was derived from the Horde_Config_Dom and related classes which
+ * in turn was derived from code written by Alexandre Alapetite. The only
+ * changes made to the original code were to implement Horde's coding
+ * standards and some minor changes to more easily fit into the framework.
+ * The original code can be found at:
+ * http://alexandre.alapetite.net/doc-alex/domxml-php4-php5/
+ *
+ * $Horde: framework/DOM/DOM.php,v 1.14 2009/10/14 16:18:22 jan Exp $
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @since   Horde 3.2
+ * @package Horde_DOM
+ */
+
+/** PEAR */
+require_once 'PEAR.php';
+
+/** Validate against the DTD */
+define('HORDE_DOM_LOAD_VALIDATE', 1);
+
+/** Recover from load errors */
+define('HORDE_DOM_LOAD_RECOVER', 2);
+
+/** Remove redundant whitespace */
+define('HORDE_DOM_LOAD_REMOVE_BLANKS', 4);
+
+/** Substitute XML entities */
+define('HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES', 8);
+
+class Horde_DOM_Document extends Horde_DOM_Node {
+
+    /**
+     * Creates an appropriate object based on the version of PHP that is
+     * running and the requested xml source. This function should be passed an
+     * array containing either 'filename' => $filename | 'xml' => $xmlstring
+     * depending on the source of the XML document.
+     *
+     * You can pass an optional 'options' parameter to enable extra
+     * features like DTD validation or removal of whitespaces.
+     * For a list of available features see the HORDE_DOM_LOAD defines.
+     * Multiple options are added by bitwise OR.
+     *
+     * @param array $params  Array containing either 'filename' | 'xml' keys.
+     *                       You can specify an optional 'options' key.
+     *
+     * @return mixed   PHP 4 domxml document | Horde_DOM_Document | PEAR_Error
+     */
+    function factory($params = array())
+    {
+        if (!isset($params['options'])) {
+            $params['options'] = 0;
+        }
+
+        if (version_compare(PHP_VERSION, '5', '>=')) {
+            // PHP 5 with Horde_DOM. Let Horde_DOM determine
+            // if we are a file or string.
+            $doc = new Horde_DOM_Document($params);
+            if (isset($doc->error)) {
+                return $doc->error;
+            }
+            return $doc;
+        }
+
+        // Load mode
+        if ($params['options'] & HORDE_DOM_LOAD_VALIDATE) {
+            $options = DOMXML_LOAD_VALIDATING;
+        } elseif ($params['options'] & HORDE_DOM_LOAD_RECOVER) {
+            $options = DOMXML_LOAD_RECOVERING;
+        } else {
+            $options = DOMXML_LOAD_PARSING;
+        }
+
+        // Load options
+        if ($params['options'] & HORDE_DOM_LOAD_REMOVE_BLANKS) {
+            $options |= DOMXML_LOAD_DONT_KEEP_BLANKS;
+        }
+        if ($params['options'] & HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES) {
+            $options |= DOMXML_LOAD_SUBSTITUTE_ENTITIES;
+        }
+
+        if (isset($params['filename'])) {
+            if (function_exists('domxml_open_file')) {
+                // PHP 4 with domxml and XML file
+                return domxml_open_file($params['filename'], $options);
+            }
+        } elseif (isset($params['xml'])) {
+            if (function_exists('domxml_open_mem')) {
+                // PHP 4 with domxml and XML string.
+                $result = @domxml_open_mem($params['xml'], $options);
+                if (!$result) {
+                    return PEAR::raiseError('Failed loading XML document.');
+                }
+                return $result;
+            }
+        } elseif (function_exists('domxml_new_doc')) {
+            // PHP 4 creating a blank doc.
+            return domxml_new_doc('1.0');
+        }
+
+        // No DOM support - raise error.
+        return PEAR::raiseError('DOM support not present.');
+    }
+
+    /**
+     * Constructor.  Determine if we are trying to load a file or xml string
+     * based on the parameters.
+     *
+     * @param array $params  Array with key 'filename' | 'xml'
+     */
+    function Horde_DOM_Document($params = array())
+    {
+        $this->node = new DOMDocument();
+
+        // Load mode
+        if ($params['options'] & HORDE_DOM_LOAD_VALIDATE) {
+            $this->node->validateOnParse = true;
+        } elseif ($params['options'] & HORDE_DOM_LOAD_RECOVER) {
+            $this->node->recover = true;
+        }
+
+        // Load options
+        if ($params['options'] & HORDE_DOM_LOAD_REMOVE_BLANKS) {
+            $this->node->preserveWhiteSpace = false;
+        }
+        if ($params['options'] & HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES) {
+            $this->node->substituteEntities = true;
+        }
+
+        if (isset($params['xml'])) {
+            $result = @$this->node->loadXML($params['xml']);
+            if (!$result) {
+                $this->error = PEAR::raiseError('Failed loading XML document.');
+                return;
+            }
+        } elseif (isset($params['filename'])) {
+            $this->node->load($params['filename']);
+        }
+        $this->document = $this;
+    }
+
+    /**
+     * Return the root document element.
+     */
+    function root()
+    {
+        return new Horde_DOM_Element($this->node->documentElement, $this);
+    }
+
+    /**
+     * Return the document element.
+     */
+    function document_element()
+    {
+        return $this->root();
+    }
+
+    /**
+     * Return the node represented by the requested tagname.
+     *
+     * @param string $name  The tagname requested.
+     *
+     * @return array The nodes matching the tag name
+     */
+    function get_elements_by_tagname($name)
+    {
+        $list = $this->node->getElementsByTagName($name);
+        $nodes = array();
+        $i = 0;
+        if (isset($list)) {
+            while ($node = $list->item($i)) {
+                $nodes[] = $this->_newDOMElement($node, $this);
+                ++$i;
+            }
+            return $nodes;
+        }
+    }
+
+    /**
+     * Return the document as a text string.
+     *
+     * @param bool   $format    Specifies whether the output should be
+     *                          neatly formatted, or not
+     * @param string $encoding  Sets the encoding attribute in line
+     *                          <?xml version="1.0" encoding="iso-8859-1"?>
+     *
+     * @return string The xml document as a string
+     */
+    function dump_mem($format = false, $encoding = false)
+    {
+        $format0 = $this->node->formatOutput;
+        $this->node->formatOutput = $format;
+
+        $encoding0 = $this->node->encoding;
+        if ($encoding) {
+            $this->node->encoding=$encoding;
+        }
+
+        $dump = $this->node->saveXML();
+
+        $this->node->formatOutput = $format0;
+
+        // UTF-8 is the default encoding for XML.
+        if ($encoding) {
+            $this->node->encoding = $encoding0 == '' ? 'UTF-8' : $encoding0;
+        }
+
+        return $dump;
+    }
+
+    /**
+     * Create a new element for this document
+     *
+     * @param string $name  Name of the new element
+     *
+     * @return Horde_DOM_Element  New element
+     */
+    function create_element($name)
+    {
+        return new Horde_DOM_Element($this->node->createElement($name), $this);
+    }
+
+    /**
+     * Create a new text node for this document
+     *
+     * @param string $content  The content of the text element
+     *
+     * @return Horde_DOM_Node  New node
+     */
+    function create_text_node($content)
+    {
+        return new Horde_DOM_Text($this->node->createTextNode($content), $this);
+    }
+
+    /**
+     * Create a new attribute for this document
+     *
+     * @param string $name   The name of the attribute
+     * @param string $value  The value of the attribute
+     *
+     * @return Horde_DOM_Attribute  New attribute
+     */
+    function create_attribute($name, $value)
+    {
+        $attr = $this->node->createAttribute($name);
+        $attr->value = $value;
+        return new Horde_DOM_Attribute($attr, $this);
+    }
+
+    function xpath_new_context()
+    {
+        return Horde_DOM_XPath::factory($this->node);
+    }
+}
+
+/**
+ * @package Horde_DOM
+ */
+class Horde_DOM_Node {
+
+    var $node;
+    var $document;
+
+    /**
+     * Wrap a DOMNode into the Horde_DOM_Node class.
+     *
+     * @param DOMNode            $node      The DOMXML node
+     * @param Horde_DOM_Document $document  The parent document
+     *
+     * @return Horde_DOM_Node  The wrapped node
+     */
+    function Horde_DOM_Node($domNode, $domDocument = null)
+    {
+        $this->node = $domNode;
+        $this->document = $domDocument;
+    }
+
+    function __get($name)
+    {
+        switch ($name) {
+        case 'type':
+            return $this->node->nodeType;
+
+        case 'tagname':
+            return $this->node->tagName;
+
+        case 'content':
+            return $this->node->textContent;
+
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Set the content of this node.
+     *
+     * @param string $text The new content of this node.
+     *
+     * @return DOMNode  The modified node.
+     */
+    function set_content($text)
+    {
+        return $this->node->appendChild($this->node->ownerDocument->createTextNode($text));
+    }
+
+    /**
+     * Return the type of this node.
+     *
+     * @return integer  Type of this node.
+     */
+    function node_type()
+    {
+        return $this->node->nodeType;
+    }
+
+    function child_nodes()
+    {
+        $nodes = array();
+
+        $nodeList = $this->node->childNodes;
+        if (isset($nodeList)) {
+            $i = 0;
+            while ($node = $nodeList->item($i)) {
+                $nodes[] = $this->_newDOMElement($node, $this->document);
+                ++$i;
+            }
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Return the attributes of this node.
+     *
+     * @return array  Attributes of this node.
+     */
+    function attributes()
+    {
+        $attributes = array();
+
+        $attrList = $this->node->attributes;
+        if (isset($attrList)) {
+            $i = 0;
+            while ($attribute = $attrList->item($i)) {
+                $attributes[] = new Horde_DOM_Attribute($attribute, $this->document);
+                ++$i;
+            }
+        }
+
+        return $attributes;
+    }
+
+    function first_child()
+    {
+        return $this->_newDOMElement($this->node->firstChild, $this->document);
+    }
+
+    /**
+     * Return the content of this node.
+     *
+     * @return string  Text content of this node.
+     */
+    function get_content()
+    {
+        return $this->node->textContent;
+    }
+
+    function has_child_nodes()
+    {
+        return $this->node->hasChildNodes();
+    }
+
+    function next_sibling()
+    {
+        if ($this->node->nextSibling === null) {
+            return null;
+        }
+
+        return $this->_newDOMElement($this->node->nextSibling, $this->document);
+    }
+
+    function node_value()
+    {
+        return $this->node->nodeValue;
+    }
+
+    function node_name()
+    {
+        if ($this->node->nodeType == XML_ELEMENT_NODE) {
+            return $this->node->localName;
+        } else {
+            return $this->node->nodeName;
+        }
+    }
+
+    function clone_node()
+    {
+        return new Horde_DOM_Node($this->node->cloneNode());
+    }
+
+    /**
+     * Append a new node
+     *
+     * @param Horde_DOM_Node $newnode  The child to be added
+     *
+     * @return Horde_DOM_Node  The resulting node
+     */
+    function append_child($newnode)
+    {
+        return $this->_newDOMElement($this->node->appendChild($this->_importNode($newnode)), $this->document);
+    }
+
+    /**
+     * Remove a child node
+     *
+     * @param Horde_DOM_Node $oldchild  The child to be removed
+     *
+     * @return Horde_DOM_Node  The resulting node
+     */
+    function remove_child($oldchild)
+    {
+        return $this->_newDOMElement($this->node->removeChild($oldchild->node), $this->document);
+    }
+
+    /**
+     * Return a node of this class type.
+     *
+     * @param DOMNode            $node      The DOMXML node
+     * @param Horde_DOM_Document $document  The parent document
+     *
+     * @return Horde_DOM_Node  The wrapped node
+     */
+    function _newDOMElement($node, $document)
+    {
+        if ($node == null) {
+            return null;
+        }
+
+        switch ($node->nodeType) {
+        case XML_ELEMENT_NODE:
+            return new Horde_DOM_Element($node, $document);
+        case XML_TEXT_NODE:
+            return new Horde_DOM_Text($node, $document);
+        case XML_ATTRIBUTE_NODE:
+            return new Horde_DOM_Attribute($node, $document);
+        // case XML_PI_NODE:
+        //     return new Horde_DOM_ProcessingInstruction($node, $document);
+        default:
+            return new Horde_DOM_Node($node, $document);
+        }
+    }
+
+    /**
+     * Private function to import DOMNode from another DOMDocument.
+     *
+     * @param Horde_DOM_Node  $newnode The node to be imported
+     *
+     * @return Horde_DOM_Node  The wrapped node
+     */
+    function _importNode($newnode)
+    {
+        if ($this->document === $newnode->document) {
+            return $newnode->node;
+        } else {
+            return $this->document->node->importNode($newnode->node, true);
+        }
+    }
+
+}
+
+/**
+ * @package Horde_DOM
+ */
+class Horde_DOM_Element extends Horde_DOM_Node {
+
+    /**
+     * Get the value of specified attribute.
+     *
+     * @param string $name  Name of the attribute
+     *
+     * @return string  Indicates if the attribute was set.
+     */
+    function get_attribute($name)
+    {
+        return $this->node->getAttribute($name);
+    }
+
+    /**
+     * Determine if an attribute of this name is present on the node.
+     *
+     * @param string $name Name of the attribute
+     *
+     * @return bool  Indicates if such an attribute is present.
+     */
+    function has_attribute($name)
+    {
+        return $this->node->hasAttribute($name);
+    }
+
+    /**
+     * Set the specified attribute on this node.
+     *
+     * @param string $name  Name of the attribute
+     * @param string $value The new value of this attribute.
+     *
+     * @return mixed  Indicates if the attribute was set.
+     */
+    function set_attribute($name, $value)
+    {
+        return $this->node->setAttribute($name, $value);
+    }
+
+    /**
+     * Return the node represented by the requested tagname.
+     *
+     * @param string $name  The tagname requested.
+     *
+     * @return array The nodes matching the tag name
+     */
+    function get_elements_by_tagname($name)
+    {
+        $list = $this->node->getElementsByTagName($name);
+        $nodes = array();
+        $i = 0;
+        if (isset($list)) {
+            while ($node = $list->item($i)) {
+                $nodes[] = $this->_newDOMElement($node, $this->document);
+                $i++;
+            }
+        }
+
+        return $nodes;
+    }
+
+}
+
+/**
+ * @package Horde_DOM
+ */
+class Horde_DOM_Attribute extends Horde_DOM_Node {
+
+    /**
+     * Return a node of this class type.
+     *
+     * @param DOMNode             $node      The DOMXML node
+     * @param Horde_DOM_Document  $document  The parent document
+     *
+     * @return Horde_DOM_Attribute  The wrapped attribute
+     */
+    function _newDOMElement($node, $document)
+    {
+        return new Horde_DOM_Attribute($node, $document);
+    }
+
+}
+
+/**
+ * @package Horde_DOM
+ */
+class Horde_DOM_Text extends Horde_DOM_Node {
+
+    function __get($name)
+    {
+        if ($name == 'tagname') {
+            return '#text';
+        } else {
+            return parent::__get($name);
+        }
+    }
+
+    function tagname()
+    {
+        return '#text';
+    }
+
+    /**
+     * Set the content of this node.
+     *
+     * @param string $text  The new content of this node.
+     */
+    function set_content($text)
+    {
+        $this->node->nodeValue = $text;
+    }
+
+}
+
+/**
+ * @package Horde_DOM
+ */
+class Horde_DOM_XPath {
+
+    /**
+     * @var DOMXPath
+     */
+    var $_xpath;
+
+    function factory($dom)
+    {
+        if (version_compare(PHP_VERSION, '5', '>=')) {
+            // PHP 5 with Horde_DOM.
+            return new Horde_DOM_XPath($dom);
+        }
+
+        return $dom->xpath_new_context();
+    }
+
+    function Horde_DOM_XPath($dom)
+    {
+        $this->_xpath = new DOMXPath($dom);
+    }
+
+    function xpath_register_ns($prefix, $uri)
+    {
+        $this->_xpath->registerNamespace($prefix, $uri);
+    }
+
+    function xpath_eval($expression, $context = null)
+    {
+        if (is_null($context)) {
+            $nodeset = $this->_xpath->query($expression);
+        } else {
+            $nodeset = $this->_xpath->query($expression, $context);
+        }
+        $result = new stdClass();
+        $result->nodeset = array();
+        for ($i = 0; $i < $nodeset->length; $i++) {
+            $result->nodeset[] = new Horde_DOM_Element($nodeset->item($i));
+        }
+        return $result;
+    }
+
+}
diff --git a/framework/DOM/package.xml b/framework/DOM/package.xml
new file mode 100644 (file)
index 0000000..b62beb3
--- /dev/null
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>DOM</name>
+ <channel>pear.horde.org</channel>
+ <summary>Wrapper classes for PHP4 domxml compatibility using PHP5&apos;s dom functions.</summary>
+ <description>These classes allow the use of code written for PHP4&apos;s domxml
+implementation to work using PHP5&apos;s dom implementation.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Michael Rubinsky</name>
+  <user>mrubinsk</user>
+  <email>mrubinsk@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-12-31</date>
+ <version>
+  <release>0.2.0</release>
+  <api>0.2.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Add Horde_DOM_Text class.
+* Add Horde_DOM_XPath class for XPath wrapping.
+* Return correct object types when iterating through child nodes.
+* Pass unit test with PHP 4.</notes>
+ <contents>
+  <dir name="/">
+   <file baseinstalldir="/Horde" name="DOM.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2008-01-21</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Allow creation of empty documents
+* Add Horde_DOM_Document::document_element()
+* Add Horde_DOM_Node::clone_node()
+* Add configuration options (Request #5370, Thomas Jarosch &lt;thomas.jarosch@intra2net.com&gt;)</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2006-05-08</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Converted to package.xml 2.0 for pear.horde.org</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2006-01-30</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Move out of Horde_Config and added DOM_Node::getElementsByTagName, 
+and DOM_Node::Node_Name methods.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/DOM/tests/dom_load_error.phpt b/framework/DOM/tests/dom_load_error.phpt
new file mode 100644 (file)
index 0000000..9116d5f
--- /dev/null
@@ -0,0 +1,34 @@
+--TEST--
+Check that Horde::DOM handles load errors gracefully.
+--FILE--
+<?php
+
+if (defined('E_DEPRECATED')) {
+    error_reporting(E_ALL & ~E_DEPRECATED);
+}
+
+require_once dirname(__FILE__) . '/../DOM.php';
+
+// Load XML
+$xml = file_get_contents(dirname(__FILE__) . '/fixtures/load_error.xml');
+
+$params = array('xml' => $xml, 'options' => HORDE_DOM_LOAD_REMOVE_BLANKS);
+
+$dom = Horde_DOM_Document::factory($params);
+
+// Check that the xml loading elicits an error
+var_dump(is_a($dom, 'PEAR_Error'));
+
+// Load XML
+$xml = file_get_contents(dirname(__FILE__) . '/fixtures/load_ok.xml');
+
+$params = array('xml' => $xml, 'options' => HORDE_DOM_LOAD_REMOVE_BLANKS);
+
+$dom = Horde_DOM_Document::factory($params);
+
+// Check that the xml loading elicits an error
+var_dump(is_a($dom, 'PEAR_Error'));
+
+--EXPECT--
+bool(true)
+bool(false)
diff --git a/framework/DOM/tests/fixtures/load_error.xml b/framework/DOM/tests/fixtures/load_error.xml
new file mode 100644 (file)
index 0000000..50e397e
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0">
+  <revision>0</revision>
+  <body>Abständen führen wirþedback</body>
+</event>
diff --git a/framework/DOM/tests/fixtures/load_ok.xml b/framework/DOM/tests/fixtures/load_ok.xml
new file mode 100644 (file)
index 0000000..42d0bf2
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0">
+  <revision>0</revision>
+  <body>Abständen führen wir Feedback</body>
+</event>
diff --git a/framework/Data/Data.php b/framework/Data/Data.php
new file mode 100644 (file)
index 0000000..09987b5
--- /dev/null
@@ -0,0 +1,428 @@
+<?php
+
+require_once 'PEAR.php';
+
+/**
+ * Abstract class to handle different kinds of Data formats and to
+ * help data exchange between Horde applications and external sources.
+ *
+ * $Horde: framework/Data/Data.php,v 1.106 2009/06/25 07:01:25 slusarz Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 1.3
+ * @package Horde_Data
+ */
+class Horde_Data extends PEAR {
+
+// Import constants
+/** Import already mapped csv data.        */ const IMPORT_MAPPED = 1;
+/** Map date and time entries of csv data. */ const IMPORT_DATETIME =  2;
+/** Import generic CSV data.               */ const IMPORT_CSV = 3;
+/** Import MS Outlook data.                */ const IMPORT_OUTLOOK = 4;
+/** Import vCalendar/iCalendar data.       */ const IMPORT_ICALENDAR = 5;
+/** Import vCards.                         */ const IMPORT_VCARD = 6;
+/** Import generic tsv data.               */ const IMPORT_TSV = 7;
+/** Import Mulberry address book data      */ const IMPORT_MULBERRY = 8;
+/** Import Pine address book data.         */ const IMPORT_PINE = 9;
+/** Import file.                           */ const IMPORT_FILE = 11;
+/** Import data.                           */ const IMPORT_DATA = 12;
+
+// Export constants
+/** Export generic CSV data. */ const EXPORT_CSV = 100;
+/** Export iCalendar data.   */ const EXPORT_ICALENDAR = 101;
+/** Export vCards.           */ const EXPORT_VCARD = 102;
+/** Export TSV data.         */ const EXPORT_TSV = 103;
+/** Export Outlook CSV data. */ const EXPORT_OUTLOOKCSV = 104;
+
+    /**
+     * File extension.
+     *
+     * @var string
+     */
+    var $_extension;
+
+    /**
+     * MIME content type.
+     *
+     * @var string
+     */
+    var $_contentType = 'text/plain';
+
+    /**
+     * A list of warnings raised during the last operation.
+     *
+     * @var array
+     * @since Horde 3.1
+     */
+    var $_warnings = array();
+
+    /**
+     * Stub to import passed data.
+     */
+    function importData()
+    {
+    }
+
+    /**
+     * Stub to return exported data.
+     */
+    function exportData()
+    {
+    }
+
+    /**
+     * Stub to import a file.
+     */
+    function importFile($filename, $header = false)
+    {
+        $data = file_get_contents($filename);
+        return $this->importData($data, $header);
+    }
+
+    /**
+     * Stub to export data to a file.
+     */
+    function exportFile()
+    {
+    }
+
+    /**
+     * Tries to determine the expected newline character based on the
+     * platform information passed by the browser's agent header.
+     *
+     * @return string  The guessed expected newline characters, either \n, \r
+     *                 or \r\n.
+     */
+    function getNewline()
+    {
+        require_once 'Horde/Browser.php';
+        $browser = &Horde_Browser::singleton();
+
+        switch ($browser->getPlatform()) {
+        case 'win':
+            return "\r\n";
+
+        case 'mac':
+            return "\r";
+
+        case 'unix':
+        default:
+            return "\n";
+        }
+    }
+
+    /**
+     * Returns the full filename including the basename and extension.
+     *
+     * @param string $basename  Basename for the file.
+     *
+     * @return string  The file name.
+     */
+    function getFilename($basename)
+    {
+        return $basename . '.' . $this->_extension;
+    }
+
+    /**
+     * Returns the content type.
+     *
+     * @return string  The content type.
+     */
+    function getContentType()
+    {
+        return $this->_contentType;
+    }
+
+    /**
+     * Returns a list of warnings that have been raised during the last
+     * operation.
+     *
+     * @since Horde 3.1
+     *
+     * @return array  A (possibly empty) list of warnings.
+     */
+    function warnings()
+    {
+        return $this->_warnings;
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Data instance based on $format.
+     *
+     * @param mixed $format  The type of concrete Horde_Data subclass to
+     *                       return. If $format is an array, then we will look
+     *                       in $format[0]/lib/Data/ for the subclass
+     *                       implementation named $format[1].php.
+     *
+     * @return Horde_Data  The newly created concrete Horde_Data instance, or
+     *                     false on an error.
+     */
+    function &factory($format)
+    {
+        if (is_array($format)) {
+            $app = $format[0];
+            $format = $format[1];
+        }
+
+        $format = basename($format);
+
+        if (empty($format) || (strcmp($format, 'none') == 0)) {
+            $data = new Horde_Data();
+            return $data;
+        }
+
+        if (!empty($app)) {
+            require_once $GLOBALS['registry']->get('fileroot', $app) . '/lib/Data/' . $format . '.php';
+        } else {
+            require_once 'Horde/Data/' . $format . '.php';
+        }
+        $class = 'Horde_Data_' . $format;
+        if (class_exists($class)) {
+            $data = new $class();
+        } else {
+            $data = PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+
+        return $data;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete Horde_Data instance
+     * based on $format. It will only create a new instance if no Horde_Data
+     * instance with the same parameters currently exists.
+     *
+     * This should be used if multiple data sources (and, thus, multiple
+     * Horde_Data instances) are required.
+     *
+     * This method must be invoked as: $var = &Horde_Data::singleton()
+     *
+     * @param string $format  The type of concrete Horde_Data subclass to
+     *                        return.
+     *
+     * @return Horde_Data  The concrete Horde_Data reference, or false on an
+     *                     error.
+     */
+    function &singleton($format)
+    {
+        static $instances = array();
+
+        $signature = serialize($format);
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &Horde_Data::factory($format);
+        }
+
+        return $instances[$signature];
+    }
+
+    /**
+     * Maps a date/time string to an associative array.
+     *
+     * The method signature has changed in Horde 3.1.3.
+     *
+     * @access private
+     *
+     * @param string $date   The date.
+     * @param string $type   One of 'date', 'time' or 'datetime'.
+     * @param array $params  Two-dimensional array with additional information
+     *                       about the formatting. Possible keys are:
+     *                       - delimiter - The character that seperates the
+     *                         different date/time parts.
+     *                       - format - If 'ampm' and $date contains a time we
+     *                         assume that it is in AM/PM format.
+     *                       - order - If $type is 'datetime' the order of the
+     *                         day and time parts: -1 (timestamp), 0
+     *                         (day/time), 1 (time/day).
+     * @param integer $key   The key to use for $params.
+     *
+     * @return string  The date or time in ISO format.
+     */
+    function mapDate($date, $type, $params, $key)
+    {
+        switch ($type) {
+        case 'date':
+        case 'monthday':
+        case 'monthdayyear':
+            $dates = explode($params['delimiter'][$key], $date);
+            if (count($dates) != 3) {
+                return $date;
+            }
+            $index = array_flip(explode('/', $params['format'][$key]));
+            return $dates[$index['year']] . '-' . $dates[$index['month']] . '-' . $dates[$index['mday']];
+
+        case 'time':
+            $dates = explode($params['delimiter'][$key], $date);
+            if (count($dates) < 2 || count($dates) > 3) {
+                return $date;
+            }
+            if ($params['format'][$key] == 'ampm') {
+                if (strpos(strtolower($dates[count($dates)-1]), 'pm') !== false) {
+                    if ($dates[0] !== '12') {
+                        $dates[0] += 12;
+                    }
+                } elseif ($dates[0] == '12') {
+                    $dates[0] = '0';
+                }
+                $dates[count($dates) - 1] = sprintf('%02d', $dates[count($dates)-1]);
+            }
+            return $dates[0] . ':' . $dates[1] . (count($dates) == 3 ? (':' . $dates[2]) : ':00');
+
+        case 'datetime':
+            switch ($params['order'][$key]) {
+            case -1:
+                return (string)(int)$date == $date
+                    ? date('Y-m-d H:i:s', $date)
+                    : $date;
+            case 0:
+                list($day, $time) = explode(' ', $date, 2);
+                break;
+            case 1:
+               list($time, $day) = explode(' ', $date, 2);
+               break;
+            }
+            $date = $this->mapDate($day, 'date',
+                                   array('delimiter' => $params['day_delimiter'],
+                                         'format' => $params['day_format']),
+                                   $key);
+            $time = $this->mapDate($time, 'time',
+                                   array('delimiter' => $params['time_delimiter'],
+                                         'format' => $params['time_format']),
+                                   $key);
+            return $date . ' ' . $time;
+
+        }
+    }
+
+    /**
+     * Takes all necessary actions for the given import step, parameters and
+     * form values and returns the next necessary step.
+     *
+     * @param integer $action  The current step. One of the IMPORT_* constants.
+     * @param array $param     An associative array containing needed
+     *                         parameters for the current step.
+     *
+     * @return mixed  Either the next step as an integer constant or imported
+     *                data set after the final step.
+     */
+    function nextStep($action, $param = array())
+    {
+        /* First step. */
+        if (is_null($action)) {
+            $_SESSION['import_data'] = array();
+            return self::IMPORT_FILE;
+        }
+
+        switch ($action) {
+        case self::IMPORT_FILE:
+            /* Sanitize uploaded file. */
+            $import_format = Horde_Util::getFormData('import_format');
+            $check_upload = Horde_Browser::wasFileUploaded('import_file', $param['file_types'][$import_format]);
+            if (is_a($check_upload, 'PEAR_Error')) {
+                return $check_upload;
+            }
+            if ($_FILES['import_file']['size'] <= 0) {
+                return PEAR::raiseError(_("The file contained no data."));
+            }
+            $_SESSION['import_data']['format'] = $import_format;
+            break;
+
+        case self::IMPORT_MAPPED:
+            $dataKeys = Horde_Util::getFormData('dataKeys', '');
+            $appKeys = Horde_Util::getFormData('appKeys', '');
+            if (empty($dataKeys) || empty($appKeys)) {
+                global $registry;
+                return PEAR::raiseError(sprintf(_("You didn't map any fields from the imported file to the corresponding fields in %s."),
+                                                $registry->get('name')));
+            }
+            $dataKeys = explode("\t", $dataKeys);
+            $appKeys = explode("\t", $appKeys);
+            $map = array();
+            $dates = array();
+            foreach ($appKeys as $key => $app) {
+                $map[$dataKeys[$key]] = $app;
+                if (isset($param['time_fields']) &&
+                    isset($param['time_fields'][$app])) {
+                    $dates[$dataKeys[$key]]['type'] = $param['time_fields'][$app];
+                    $dates[$dataKeys[$key]]['values'] = array();
+                    $i = 0;
+                    /* Build an example array of up to 10 date/time fields. */
+                    while ($i < count($_SESSION['import_data']['data']) && count($dates[$dataKeys[$key]]['values']) < 10) {
+                        if (!empty($_SESSION['import_data']['data'][$i][$dataKeys[$key]])) {
+                            $dates[$dataKeys[$key]]['values'][] = $_SESSION['import_data']['data'][$i][$dataKeys[$key]];
+                        }
+                        $i++;
+                    }
+                }
+            }
+            $_SESSION['import_data']['map'] = $map;
+            if (count($dates) > 0) {
+                foreach ($dates as $key => $data) {
+                    if (count($data['values'])) {
+                        $_SESSION['import_data']['dates'] = $dates;
+                        return self::IMPORT_DATETIME;
+                    }
+                }
+            }
+            return $this->nextStep(self::IMPORT_DATA, $param);
+
+        case self::IMPORT_DATETIME:
+        case self::IMPORT_DATA:
+            if ($action == self::IMPORT_DATETIME) {
+                $params = array('delimiter' => Horde_Util::getFormData('delimiter'),
+                                'format' => Horde_Util::getFormData('format'),
+                                'order' => Horde_Util::getFormData('order'),
+                                'day_delimiter' => Horde_Util::getFormData('day_delimiter'),
+                                'day_format' => Horde_Util::getFormData('day_format'),
+                                'time_delimiter' => Horde_Util::getFormData('time_delimiter'),
+                                'time_format' => Horde_Util::getFormData('time_format'));
+            }
+            if (!isset($_SESSION['import_data']['data'])) {
+                return PEAR::raiseError(_("The uploaded data was lost since the previous step."));
+            }
+            /* Build the result data set as an associative array. */
+            $data = array();
+            foreach ($_SESSION['import_data']['data'] as $row) {
+                $data_row = array();
+                foreach ($row as $key => $val) {
+                    if (isset($_SESSION['import_data']['map'][$key])) {
+                        $mapped_key = $_SESSION['import_data']['map'][$key];
+                        if ($action == self::IMPORT_DATETIME &&
+                            !empty($val) &&
+                            isset($param['time_fields']) &&
+                            isset($param['time_fields'][$mapped_key])) {
+                            $val = $this->mapDate($val, $param['time_fields'][$mapped_key], $params, $key);
+                        }
+                        $data_row[$_SESSION['import_data']['map'][$key]] = $val;
+                    }
+                }
+                $data[] = $data_row;
+            }
+            return $data;
+        }
+    }
+
+    /**
+     * Cleans the session data up and removes any uploaded and moved
+     * files. If a function called "_cleanup()" exists, this gets
+     * called too.
+     *
+     * @return mixed  If _cleanup() was called, the return value of this call.
+     *                This should be the value of the first import step.
+     */
+    function cleanup()
+    {
+        if (isset($_SESSION['import_data']['file_name'])) {
+            @unlink($_SESSION['import_data']['file_name']);
+        }
+        $_SESSION['import_data'] = array();
+        if (function_exists('_cleanup')) {
+            return _cleanup();
+        }
+    }
+
+}
diff --git a/framework/Data/Data/csv.php b/framework/Data/Data/csv.php
new file mode 100644 (file)
index 0000000..4e24287
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+/**
+ * @package Horde_Data
+ */
+
+/**
+ * Horde's File_CSV class.
+ */
+include_once 'File/CSV.php';
+
+/**
+ * Horde_Data implementation for comma-separated data (CSV).
+ *
+ * $Horde: framework/Data/Data/csv.php,v 1.49 2009/07/09 08:17:54 slusarz Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 1.3
+ * @package Horde_Data
+ */
+class Horde_Data_csv extends Horde_Data {
+
+    var $_extension = 'csv';
+    var $_contentType = 'text/comma-separated-values';
+
+    /**
+     * Tries to discover the CSV file's parameters.
+     *
+     * @param string $filename  The name of the file to investigate.
+     *
+     * @return array  An associative array with the following possible keys:
+     * <pre>
+     * 'sep':    The field separator
+     * 'quote':  The quoting character
+     * 'fields': The number of fields (columns)
+     * </pre>
+     */
+    function discoverFormat($filename)
+    {
+        return File_CSV::discoverFormat($filename);
+    }
+
+    /**
+     * Imports and parses a CSV file.
+     *
+     * @param string $filename  The name of the file to parse.
+     * @param boolean $header   Does the first line contain the field/column
+     *                          names?
+     * @param string $sep       The field/column separator.
+     * @param string $quote     The quoting character.
+     * @param integer $fields   The number or fields/columns.
+     * @param string $charset   The file's charset. @since Horde 3.1.
+     * @param string $crlf      The file's linefeed characters. @since Horde 3.1.
+     *
+     * @return array  A two-dimensional array of all imported data rows.  If
+     *                $header was true the rows are associative arrays with the
+     *                field/column names as the keys.
+     */
+    function importFile($filename, $header = false, $sep = '', $quote = '',
+                        $fields = null, $import_mapping = array(),
+                        $charset = null, $crlf = null)
+    {
+        /* File_CSV is a bit picky at what parameters it expects. */
+        $conf = array();
+        if ($fields) {
+            $conf['fields'] = $fields;
+        } else {
+            return array();
+        }
+        if (!empty($quote)) {
+            $conf['quote'] = $quote;
+        }
+        if (empty($sep)) {
+            $conf['sep'] = ',';
+        } else {
+            $conf['sep'] = $sep;
+        }
+        if (!empty($crlf)) {
+            $conf['crlf'] = $crlf;
+        }
+
+        /* Strip and keep the first line if it contains the field
+         * names. */
+        if ($header) {
+            $head = File_CSV::read($filename, $conf);
+            if (is_a($head, 'PEAR_Error')) {
+                return $head;
+            }
+            if (!empty($charset)) {
+                $head = Horde_String::convertCharset($head, $charset, Horde_Nls::getCharset());
+            }
+        }
+
+        $data = array();
+        while ($line = File_CSV::read($filename, $conf)) {
+            if (is_a($line, 'PEAR_Error')) {
+                return $line;
+            }
+            if (!empty($charset)) {
+                $line = Horde_String::convertCharset($line, $charset, Horde_Nls::getCharset());
+            }
+            if (!isset($head)) {
+                $data[] = $line;
+            } else {
+                $newline = array();
+                for ($i = 0; $i < count($head); $i++) {
+                    if (isset($import_mapping[$head[$i]])) {
+                        $head[$i] = $import_mapping[$head[$i]];
+                    }
+                    $cell = $line[$i];
+                    $cell = preg_replace("/\"\"/", "\"", $cell);
+                    $newline[$head[$i]] = empty($cell) ? '' : $cell;
+                }
+                $data[] = $newline;
+            }
+        }
+
+        $fp = File_CSV::getPointer($filename, $conf);
+        if ($fp && !is_a($fp, 'PEAR_Error')) {
+            rewind($fp);
+        }
+
+        $this->_warnings = File_CSV::warning();
+        return $data;
+    }
+
+    /**
+     * Builds a CSV file from a given data structure and returns it as a
+     * string.
+     *
+     * @param array $data      A two-dimensional array containing the data set.
+     * @param boolean $header  If true, the rows of $data are associative
+     *                         arrays with field names as their keys.
+     *
+     * @return string  The CSV data.
+     */
+    function exportData($data, $header = false, $export_mapping = array())
+    {
+        if (!is_array($data) || count($data) == 0) {
+            return '';
+        }
+
+        $export = '';
+        $eol = "\n";
+        $head = array_keys(current($data));
+        if ($header) {
+            foreach ($head as $key) {
+                if (!empty($key)) {
+                    if (isset($export_mapping[$key])) {
+                        $key = $export_mapping[$key];
+                    }
+                    $export .= '"' . $key . '"';
+                }
+                $export .= ',';
+            }
+            $export = substr($export, 0, -1) . $eol;
+        }
+
+        foreach ($data as $row) {
+            foreach ($head as $key) {
+                $cell = $row[$key];
+                if (!empty($cell) || $cell === 0) {
+                    $export .= '"' . $cell . '"';
+                }
+                $export .= ',';
+            }
+            $export = substr($export, 0, -1) . $eol;
+        }
+
+        return $export;
+    }
+
+    /**
+     * Builds a CSV file from a given data structure and triggers its
+     * download.  It DOES NOT exit the current script but only outputs the
+     * correct headers and data.
+     *
+     * @param string $filename  The name of the file to be downloaded.
+     * @param array $data       A two-dimensional array containing the data
+     *                          set.
+     * @param boolean $header   If true, the rows of $data are associative
+     *                          arrays with field names as their keys.
+     */
+    function exportFile($filename, $data, $header = false,
+                        $export_mapping = array())
+    {
+        $export = $this->exportData($data, $header, $export_mapping);
+        $GLOBALS['browser']->downloadHeaders($filename, 'application/csv', false, strlen($export));
+        echo $export;
+    }
+
+    /**
+     * Takes all necessary actions for the given import step, parameters and
+     * form values and returns the next necessary step.
+     *
+     * @param integer $action  The current step. One of the IMPORT_* constants.
+     * @param array $param     An associative array containing needed
+     *                         parameters for the current step.
+     *
+     * @return mixed  Either the next step as an integer constant or imported
+     *                data set after the final step.
+     */
+    function nextStep($action, $param = array())
+    {
+        switch ($action) {
+        case self::IMPORT_FILE:
+            $next_step = parent::nextStep($action, $param);
+            if (is_a($next_step, 'PEAR_Error')) {
+                return $next_step;
+            }
+
+            /* Move uploaded file so that we can read it again in the next
+               step after the user gave some format details. */
+            $file_name = Horde::getTempFile('import', false);
+            if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) {
+                return PEAR::raiseError(_("The uploaded file could not be saved."));
+            }
+            $_SESSION['import_data']['file_name'] = $file_name;
+
+            /* Try to discover the file format ourselves. */
+            $conf = $this->discoverFormat($file_name);
+            if (!$conf) {
+                $conf = array('sep' => ',');
+            }
+            $_SESSION['import_data'] = array_merge($_SESSION['import_data'], $conf);
+
+            /* Check if charset was specified. */
+            $_SESSION['import_data']['charset'] = Horde_Util::getFormData('charset');
+
+            /* Read the file's first two lines to show them to the user. */
+            $_SESSION['import_data']['first_lines'] = '';
+            $fp = @fopen($file_name, 'r');
+            if ($fp) {
+                $line_no = 1;
+                while ($line_no < 3 && $line = fgets($fp)) {
+                    if (!empty($_SESSION['import_data']['charset'])) {
+                        $line = Horde_String::convertCharset($line, $_SESSION['import_data']['charset'], Horde_Nls::getCharset());
+                    }
+                    $newline = Horde_String::length($line) > 100 ? "\n" : '';
+                    $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline;
+                    $line_no++;
+                }
+            }
+            return self::IMPORT_CSV;
+
+        case self::IMPORT_CSV:
+            $_SESSION['import_data']['header'] = Horde_Util::getFormData('header');
+            $import_mapping = array();
+            if (isset($param['import_mapping'])) {
+                $import_mapping = $param['import_mapping'];
+            }
+            $import_data = $this->importFile($_SESSION['import_data']['file_name'],
+                                             $_SESSION['import_data']['header'],
+                                             Horde_Util::getFormData('sep'),
+                                             Horde_Util::getFormData('quote'),
+                                             Horde_Util::getFormData('fields'),
+                                             $import_mapping,
+                                             $_SESSION['import_data']['charset'],
+                                             $_SESSION['import_data']['crlf']);
+            $_SESSION['import_data']['data'] = $import_data;
+            unset($_SESSION['import_data']['map']);
+            return self::IMPORT_MAPPED;
+
+        default:
+            return parent::nextStep($action, $param);
+        }
+    }
+
+}
diff --git a/framework/Data/Data/icalendar.php b/framework/Data/Data/icalendar.php
new file mode 100644 (file)
index 0000000..8211d6e
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+/** We rely on the Horde_Data_imc:: abstract class. */
+require_once dirname(__FILE__) . '/imc.php';
+
+/**
+ * This is iCalendar (vCalendar).
+ *
+ * $Horde: framework/Data/Data/icalendar.php,v 1.37 2009/01/06 17:49:13 jan Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Karsten Fourmont <fourmont@horde.org>
+ * @package Horde_Data
+ * @since   Horde 3.0
+ */
+class Horde_Data_icalendar extends Horde_Data_imc {
+
+}
diff --git a/framework/Data/Data/imc.php b/framework/Data/Data/imc.php
new file mode 100644 (file)
index 0000000..3945b55
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+
+require_once 'Horde/iCalendar.php';
+
+/**
+ * Abstract implementation of the Horde_Data:: API for IMC data -
+ * vCards and iCalendar data, etc. Provides a number of utility
+ * methods that vCard and iCalendar implementation can share and rely
+ * on.
+ *
+ * $Horde: framework/Data/Data/imc.php,v 1.44 2009/02/09 23:40:46 mrubinsk Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Data
+ * @since   Horde 3.0
+ */
+class Horde_Data_imc extends Horde_Data {
+
+    var $_iCal = false;
+
+    function importData($text)
+    {
+        $this->_iCal = new Horde_iCalendar();
+        if (!$this->_iCal->parsevCalendar($text)) {
+            return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+        }
+
+        return $this->_iCal->getComponents();
+    }
+
+    /**
+     * Builds an iCalendar file from a given data structure and
+     * returns it as a string.
+     *
+     * @param array $data     An array containing Horde_iCalendar_vevent
+     *                        objects
+     * @param string $method  The iTip method to use.
+     *
+     * @return string  The iCalendar data.
+     */
+    function exportData($data, $method = 'REQUEST')
+    {
+        $this->_iCal = new Horde_iCalendar();
+        $this->_iCal->setAttribute('METHOD', $method);
+
+        foreach ($data as $event) {
+            $this->_iCal->addComponent($event);
+        }
+
+        return $this->_iCal->exportvCalendar();
+    }
+
+    /**
+     * Builds an iCalendar file from a given data structure and
+     * triggers its download.  It DOES NOT exit the current script but
+     * only outputs the correct headers and data.
+     *
+     * @param string $filename   The name of the file to be downloaded.
+     * @param array $data        An array containing Horde_iCalendar_vevents
+     */
+    function exportFile($filename, $data)
+    {
+        $export = $this->exportData($data);
+        $GLOBALS['browser']->downloadHeaders($filename, 'text/calendar', false, strlen($export));
+        echo $export;
+    }
+
+    /**
+     * Takes all necessary actions for the given import step,
+     * parameters and form values and returns the next necessary step.
+     *
+     * @param integer $action  The current step. One of the IMPORT_* constants.
+     * @param array $param     An associative array containing needed
+     *                         parameters for the current step.
+     * @return mixed  Either the next step as an integer constant or imported
+     *                data set after the final step.
+     */
+    function nextStep($action, $param = array())
+    {
+        switch ($action) {
+        case self::IMPORT_FILE:
+            $next_step = parent::nextStep($action, $param);
+            if (is_a($next_step, 'PEAR_Error')) {
+                return $next_step;
+            }
+
+            $import_data = $this->importFile($_FILES['import_file']['tmp_name']);
+            if (is_a($import_data, 'PEAR_Error')) {
+                return $import_data;
+            }
+
+            return $this->_iCal->getComponents();
+            break;
+
+        default:
+            return parent::nextStep($action, $param);
+            break;
+        }
+    }
+
+}
diff --git a/framework/Data/Data/outlookcsv.php b/framework/Data/Data/outlookcsv.php
new file mode 100644 (file)
index 0000000..b686e19
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+require_once 'Horde/Data/csv.php';
+
+/**
+ * Horde_Data implementation for Outlook comma-separated data (CSV).
+ *
+ * $Horde: framework/Data/Data/outlookcsv.php,v 1.5 2005/07/03 04:42:36 selsky Exp $
+ *
+ * @package Horde_Data
+ */
+class Horde_Data_outlookcsv extends Horde_Data_csv {
+
+    /**
+     * Builds a CSV file from a given data structure and returns it as a
+     * string.
+     *
+     * @param array   $data       A two-dimensional array containing the data
+     *                            set.
+     * @param boolean $header     If true, the rows of $data are associative
+     *                            arrays with field names as their keys.
+     *
+     * @return string  The CSV data.
+     */
+    function exportData($data, $header = false, $export_mapping = array())
+    {
+        if (!is_array($data) || count($data) == 0) {
+            return '';
+        }
+
+        $export = '';
+        $eol = "\r\n";
+        $head = array_keys(current($data));
+        if ($header) {
+            foreach ($head as $key) {
+                if (!empty($key)) {
+                    if (isset($export_mapping[$key])) {
+                        $key = $export_mapping[$key];
+                    }
+                    $export .= '"' . $key . '"';
+                }
+                $export .= ',';
+            }
+            $export = substr($export, 0, -1) . $eol;
+        }
+
+        foreach ($data as $row) {
+            foreach ($head as $key) {
+                $cell = $row[$key];
+                if (!empty($cell) || $cell === 0) {
+                    $cell = preg_replace("/\"/", "\"\"", $cell);
+                    $export .= '"' . $cell . '"';
+                }
+                $export .= ',';
+            }
+            $export = substr($export, 0, -1) . $eol;
+        }
+
+        return $export;
+    }
+
+}
diff --git a/framework/Data/Data/tsv.php b/framework/Data/Data/tsv.php
new file mode 100644 (file)
index 0000000..0736a7e
--- /dev/null
@@ -0,0 +1,228 @@
+<?php
+/**
+ * Horde_Data implementation for tab-separated data (TSV).
+ *
+ * $Horde: framework/Data/Data/tsv.php,v 1.44 2009/06/09 23:23:33 slusarz Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 1.3
+ * @package Horde_Data
+ */
+class Horde_Data_tsv extends Horde_Data {
+
+    var $_extension = 'tsv';
+    var $_contentType = 'text/tab-separated-values';
+
+    /**
+     * Convert data file contents to list of data records.
+     *
+     * @param string $contents   Data file contents.
+     * @param boolean $header    True if a header row is present.
+     * @param string $delimiter  Field delimiter.
+     *
+     * @return array  List of data records.
+     */
+    function importData($contents, $header = false, $delimiter = "\t")
+    {
+        if ($_SESSION['import_data']['format'] == 'pine') {
+            $contents = preg_replace('/\n +/', '', $contents);
+        }
+        $contents = explode("\n", $contents);
+        $data = array();
+        if ($header) {
+            $head = explode($delimiter, array_shift($contents));
+        }
+        foreach ($contents as $line) {
+            if (trim($line) == '') {
+                continue;
+            }
+            $line = explode($delimiter, $line);
+            if (!isset($head)) {
+                $data[] = $line;
+            } else {
+                $newline = array();
+                for ($i = 0; $i < count($head); $i++) {
+                    $newline[$head[$i]] = empty($line[$i]) ? '' : $line[$i];
+                }
+                $data[] = $newline;
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Builds a TSV file from a given data structure and returns it as a
+     * string.
+     *
+     * @param array $data      A two-dimensional array containing the data set.
+     * @param boolean $header  If true, the rows of $data are associative
+     *                         arrays with field names as their keys.
+     *
+     * @return string  The TSV data.
+     */
+    function exportData($data, $header = false)
+    {
+        if (!is_array($data) || count($data) == 0) {
+            return '';
+        }
+        $export = '';
+        $head = array_keys(current($data));
+        if ($header) {
+            $export = implode("\t", $head) . "\n";
+        }
+        foreach ($data as $row) {
+            foreach ($head as $key) {
+                $cell = $row[$key];
+                if (!empty($cell) || $cell === 0) {
+                    $export .= $cell;
+                }
+                $export .= "\t";
+            }
+            $export = substr($export, 0, -1) . "\n";
+        }
+        return $export;
+    }
+
+    /**
+     * Builds a TSV file from a given data structure and triggers its download.
+     * It DOES NOT exit the current script but only outputs the correct headers
+     * and data.
+     *
+     * @param string $filename  The name of the file to be downloaded.
+     * @param array $data       A two-dimensional array containing the data
+     *                          set.
+     * @param boolean $header   If true, the rows of $data are associative
+     *                          arrays with field names as their keys.
+     */
+    function exportFile($filename, $data, $header = false)
+    {
+        $export = $this->exportData($data, $header);
+        $GLOBALS['browser']->downloadHeaders($filename, 'text/tab-separated-values', false, strlen($export));
+        echo $export;
+    }
+
+    /**
+     * Takes all necessary actions for the given import step, parameters and
+     * form values and returns the next necessary step.
+     *
+     * @param integer $action  The current step. One of the IMPORT_* constants.
+     * @param array $param     An associative array containing needed
+     *                         parameters for the current step.
+     *
+     * @return mixed  Either the next step as an integer constant or imported
+     *                data set after the final step.
+     */
+    function nextStep($action, $param = array())
+    {
+        switch ($action) {
+        case self::IMPORT_FILE:
+            $next_step = parent::nextStep($action, $param);
+            if (is_a($next_step, 'PEAR_Error')) {
+                return $next_step;
+            }
+
+            if ($_SESSION['import_data']['format'] == 'mulberry' ||
+                $_SESSION['import_data']['format'] == 'pine') {
+                $_SESSION['import_data']['data'] = $this->importFile($_FILES['import_file']['tmp_name']);
+                $format = $_SESSION['import_data']['format'];
+                if ($format == 'mulberry') {
+                    $appKeys  = array('alias', 'name', 'email', 'company', 'workAddress', 'workPhone', 'homePhone', 'fax', 'notes');
+                    $dataKeys = array(0, 1, 2, 3, 4, 5, 6, 7, 9);
+                } elseif ($format == 'pine') {
+                    $appKeys = array('alias', 'name', 'email', 'notes');
+                    $dataKeys = array(0, 1, 2, 4);
+                }
+                foreach ($appKeys as $key => $app) {
+                    $map[$dataKeys[$key]] = $app;
+                }
+                $data = array();
+                foreach ($_SESSION['import_data']['data'] as $row) {
+                    $hash = array();
+                    if ($format == 'mulberry') {
+                        if (preg_match("/^Grp:/", $row[0]) || empty($row[1])) {
+                            continue;
+                        }
+                        $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]);
+                        foreach ($dataKeys as $key) {
+                            if (array_key_exists($key, $row)) {
+                                $hash[$key] = stripslashes(preg_replace('/\\\\r/', "\n", $row[$key]));
+                            }
+                        }
+                    } elseif ($format == 'pine') {
+                        if (count($row) < 3 || preg_match("/^#DELETED/", $row[0]) || preg_match("/[()]/", $row[2])) {
+                            continue;
+                        }
+                        $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]);
+                        /* Address can be a full RFC822 address */
+                        require_once 'Horde/MIME.php';
+                        $addr_arr = MIME::parseAddressList($row[2]);
+                        if (is_a($addr_arr, 'PEAR_Error') ||
+                            empty($addr_arr[0]->mailbox)) {
+                            continue;
+                        }
+                        $row[2] = $addr_arr[0]->mailbox . '@' . $addr_arr[0]->host;
+                        if (empty($row[1]) && !empty($addr_arr[0]->personal)) {
+                            $row[1] = $addr_arr[0]->personal;
+                        }
+                        foreach ($dataKeys as $key) {
+                            if (array_key_exists($key, $row)) {
+                                $hash[$key] = $row[$key];
+                            }
+                        }
+                    }
+                    $data[] = $hash;
+                }
+                $_SESSION['import_data']['data'] = $data;
+                $_SESSION['import_data']['map'] = $map;
+                $ret = $this->nextStep(self::IMPORT_DATA, $param);
+                return $ret;
+            }
+
+            /* Move uploaded file so that we can read it again in the next step
+               after the user gave some format details. */
+            $uploaded = Horde_Browser::wasFileUploaded('import_file', _("TSV file"));
+            if (is_a($uploaded, 'PEAR_Error')) {
+                return PEAR::raiseError($uploaded->getMessage());
+            }
+            $file_name = Horde::getTempFile('import', false);
+            if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) {
+                return PEAR::raiseError(_("The uploaded file could not be saved."));
+            }
+            $_SESSION['import_data']['file_name'] = $file_name;
+
+            /* Read the file's first two lines to show them to the user. */
+            $_SESSION['import_data']['first_lines'] = '';
+            $fp = @fopen($file_name, 'r');
+            if ($fp) {
+                $line_no = 1;
+                while ($line_no < 3 && $line = fgets($fp)) {
+                    $newline = Horde_String::length($line) > 100 ? "\n" : '';
+                    $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline;
+                    $line_no++;
+                }
+            }
+            return self::IMPORT_TSV;
+            break;
+
+        case self::IMPORT_TSV:
+            $_SESSION['import_data']['header'] = Horde_Util::getFormData('header');
+            $import_data = $this->importFile($_SESSION['import_data']['file_name'],
+                                             $_SESSION['import_data']['header']);
+            $_SESSION['import_data']['data'] = $import_data;
+            unset($_SESSION['import_data']['map']);
+            return self::IMPORT_MAPPED;
+            break;
+
+        default:
+            return parent::nextStep($action, $param);
+            break;
+        }
+    }
+
+}
diff --git a/framework/Data/Data/vcard.php b/framework/Data/Data/vcard.php
new file mode 100644 (file)
index 0000000..794b6ea
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+require_once dirname(__FILE__) . '/imc.php';
+require_once 'Horde/iCalendar.php';
+
+/**
+ * Implement the Horde_Data:: API for vCard data.
+ *
+ * $Horde: framework/Data/Data/vcard.php,v 1.43 2009/01/06 17:49:13 jan Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Data
+ */
+class Horde_Data_vcard extends Horde_Data_imc {
+
+    /**
+     * Exports vcalendar data as a string. Unlike vEvent, vCard data
+     * is not enclosed in BEGIN|END:vCalendar.
+     *
+     * @param array $data     An array containing Horde_iCalendar_vcard
+     *                        objects.
+     * @param string $method  The iTip method to use.
+     *
+     * @return string  The iCalendar data.
+     */
+    function exportData($data, $method = 'REQUEST')
+    {
+        $s = '';
+        foreach ($data as $vcard) {
+            $s.= $vcard->exportvCalendar();
+        }
+        return $s;
+    }
+
+}
diff --git a/framework/Data/Data/vnote.php b/framework/Data/Data/vnote.php
new file mode 100644 (file)
index 0000000..55c02d1
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+require_once dirname(__FILE__) . '/imc.php';
+
+/**
+ * Implement the Horde_Data:: API for vNote data.
+ *
+ * $Horde: framework/Data/Data/vnote.php,v 1.17 2009/07/14 00:25:28 mrubinsk Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ * @since   Horde 3.0
+ */
+class Horde_Data_vnote extends Horde_Data_imc {
+
+    /**
+     * Exports vcalendar data as a string. Unlike vEvent, vNote data
+     * is not enclosed in BEGIN|END:vCalendar.
+     *
+     * @param array $data     An array containing Horde_iCalendar_vnote
+     *                        objects.
+     * @param string $method  The iTip method to use.
+     *
+     * @return string  The iCalendar data.
+     */
+    function exportData($data, $method = 'REQUEST')
+    {
+        global $prefs;
+
+        $this->_iCal = new Horde_iCalendar();
+
+        $this->_iCal->setAttribute('METHOD', $method);
+        $s = '';
+        foreach ($data as $event) {
+            $s.= $event->exportvCalendar();
+        }
+        return $s;
+    }
+
+}
diff --git a/framework/Data/Data/vtodo.php b/framework/Data/Data/vtodo.php
new file mode 100644 (file)
index 0000000..6d56d6f
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+/** We rely on the Horde_Data_imc:: abstract class. */
+require_once dirname(__FILE__) . '/imc.php';
+
+/**
+ * Implement the Horde_Data:: API for vTodo data.
+ *
+ * $Horde: framework/Data/Data/vtodo.php,v 1.14 2009/01/06 17:49:13 jan Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Data
+ * @since   Horde 3.0
+ */
+class Horde_Data_vtodo extends Horde_Data_imc {
+
+}
diff --git a/framework/Data/docs/examples/Eudora.txt b/framework/Data/docs/examples/Eudora.txt
new file mode 100644 (file)
index 0000000..bdc7f04
--- /dev/null
@@ -0,0 +1,2 @@
+alias firman firman@php.net
+note firman <first:Firman><last:Wandayandi><displayname:Firman Wandayandi><email:firman@php.net><address:Foo Street><city:Bandung><zip:99999><state:West Java><country:Indonesia><phone:99999999><fax:99999999><mobile:999999999><web:http://php.hm/~firman><address2:Foo Street><city2:Bandung><zip2:99999><state2:West Java><country2:Indonesia><phone2:99999999><fax2:99999999><company:Foo><title:Web Developer>It's a notes.
diff --git a/framework/Data/docs/examples/Gmail.csv b/framework/Data/docs/examples/Gmail.csv
new file mode 100644 (file)
index 0000000..b371bed
--- /dev/null
@@ -0,0 +1,3 @@
+Name,E-mail,Notes,Section 1 - Description,Section 1 - Email,Section 1 - IM,Section 1 - Phone,Section 1 - Mobile,Section 1 - Pager,Section 1 - Fax,Section 1 - Company,Section 1 - Title,Section 1 - Other,Section 1 - Address
+John Doe,john@example.com,,,john.doe@example.com,,9999999,9999999,9999999,9999999,"Example, Corp.",Mr,,Foo Street
+Jeniffer Doe,jeniffer@example.com,,,jeniffer.doe@example.com,,9999999,9999999,9999999,9999999,"Example, Corp.",Mrs,,Foo Street
diff --git a/framework/Data/docs/examples/KMail.csv b/framework/Data/docs/examples/KMail.csv
new file mode 100644 (file)
index 0000000..bf400a4
--- /dev/null
@@ -0,0 +1,2 @@
+"Formatted Name","Family Name","Given Name","Additional Names","Honorific Prefixes","Honorific Suffixes","Nick Name","Birthday","Home Address Street","Home Address Locality","Home Address Region","Home Address Postal Code","Home Address Country","Home Address Label","Business Address Street","Business Address Locality","Business Address Region","Business Address Postal Code","Business Address Country","Business Address Label","Home Phone","Business Phone","Mobile Phone","Home Fax","Business Fax","Car Phone","ISDN","Pager","Email Address","Mail Client","Title","Role","Organization","Note","URL","Department","Profession","Assistant's Name","Manager's Name","Spouse's Name","Office","IM Address","Anniversary"
+"Firman","","Firman Wandayandi","Wandayandi","","","firman","","Foo Street","","","99999","Indonesia","","Foo Street","","","99999","Indonesia","","99999999","99999999","999999999","99999999","99999999","","","99999999","firman@php.net","","","Web Developer","Foo","It's a notes.","http://php.hm/~firman","Foo Net","","","","","","",""
diff --git a/framework/Data/docs/examples/Outlook.csv b/framework/Data/docs/examples/Outlook.csv
new file mode 100644 (file)
index 0000000..2dd1c14
--- /dev/null
@@ -0,0 +1,4 @@
+Title,First Name,Middle Name,Last Name,Suffix,Company,Department,Job Title,Business Street,Business Street 2,Business Street 3,Business City,Business State,Business Postal Code,Business Country,Home Street,Home Street 2,Home Street 3,Home City,Home State,Home Postal Code,Home Country,Other Street,Other Street 2,Other Street 3,Other City,Other State,Other Postal Code,Other Country,Assistant's Phone,Business Fax,Business Phone,Business Phone 2,Callback,Car Phone,Company Main Phone,Home Fax,Home Phone,Home Phone 2,ISDN,Mobile Phone,Other Fax,Other Phone,Pager,Primary Phone,Radio Phone,TTY/TDD Phone,Telex,Account,Anniversary,Assistant's Name,Billing Information,Birthday,Business Address PO Box,Categories,Children,Directory Server,E-mail Address,E-mail Type,E-mail Display Name,E-mail 2 Address,E-mail 2 Type,E-mail 2 Display Name,E-mail 3 Address,E-mail 3 Type,E-mail 3 Display Name,Gender,Government ID Number,Hobby,Home Address PO Box,Initials,Internet Free Busy,Keywords,Language,Location,Manager's Name,Mileage,Notes,Office Location,Organizational ID Number,Other Address PO Box,Priority,Private,Profession,Referred By,Sensitivity,Spouse,User 1,User 2,User 3,User 4,Web Page
+,Firman,,Wandayandi,,Foo,Foo Net,Web Developer,Foo Street,,,Bandung,West Java,99999,Indonesia,Foo Street,,,Bandung,West Java,99999,Indonesia,,,,,,,,,99999999,,,,,99999999,99999999,99999999,,,999999999,,,99999999,,,,,,,,,,,,,,firman@php.net,,Firman Wandayandi,,,,,,,,,,,,,,,,,,"It's
+multiline 
+memo.",,,,,,,,,,,,,,http://php.hm/~firman
diff --git a/framework/Data/docs/examples/Palm.csv b/framework/Data/docs/examples/Palm.csv
new file mode 100644 (file)
index 0000000..4fbf472
--- /dev/null
@@ -0,0 +1,2 @@
+"Shumway1","Gordon","President","Alf Ltd","408-555-1254","408-555-1255","408-555-1256","408-555-1257","Gordon!@usa.com","123 Anywhere Street","San Jose","CA","95128","USA","","","","","","0","Family"
+"Shumway2","Gordon","VP","Alf Ltd","405-555-1234","408-555-1235","408-555-1236","408-555-1237","Gordon2@usa.com","12345 Lost Street","San Jose","CA","95129","USA","","","","","","1","Family"
diff --git a/framework/Data/docs/examples/Thunderbird.csv b/framework/Data/docs/examples/Thunderbird.csv
new file mode 100644 (file)
index 0000000..0f953cd
--- /dev/null
@@ -0,0 +1 @@
+Firman,Wandayandi,Firman Wandayandi,firman,firman@php.net,,99999999,99999999,99999999,99999999,999999999,Foo Street,,Bandung,West Java,99999,,Foo Street,,Bandung,West Java,99999,Indonesia,Web Developer,Foo Net,Foo,,http://php.hm/~firman,,,,,,,,It's a notes.,
diff --git a/framework/Data/docs/examples/WAB-selectable.csv b/framework/Data/docs/examples/WAB-selectable.csv
new file mode 100644 (file)
index 0000000..f3d5974
--- /dev/null
@@ -0,0 +1,4 @@
+First Name,Last Name,Name,Nickname,E-mail Address,Home Street,Home City,Home Postal Code,Home State,Home Country/Region,Home Phone,Home Fax,Mobile Phone,Personal Web Page,Business Street,Business City,Business Postal Code,Business State,Business Country/Region,Business Web Page,Business Phone,Business Fax,Pager,Company,Job Title,Department,Office Location,Notes
+foo,bar,foo,foo,foo@bar.com,"Foo Street 8, 20 Building",foo,9999999,foo,Afghanistan,Foo Street 8,99999999,999999999,http://foo.bar.com,Foo Street 8,Foo,99999,Foo,Foo/Foo,,Foo Street 8,99999999,99999999,foo,Web Developer,Foo Net,Foo Building,"I'm
+multiline 
+memo."
diff --git a/framework/Data/docs/examples/WAB.csv b/framework/Data/docs/examples/WAB.csv
new file mode 100644 (file)
index 0000000..4e95b35
--- /dev/null
@@ -0,0 +1,4 @@
+First Name,Last Name,Middle Name,Name,Nickname,E-mail Address,Home Street,Home City,Home Postal Code,Home State,Home Country/Region,Home Phone,Home Fax,Mobile Phone,Personal Web Page,Business Street,Business City,Business Postal Code,Business State,Business Country/Region,Business Web Page,Business Phone,Business Fax,Pager,Company,Job Title,Department,Office Location,Notes
+Firman,Wandayandi,,Firman Wandayandi,firman,firman@php.net,Foo Street,Bandung,99999,West Java,Indonesia,99999999,99999999,999999999,http://php.hm/~firman,Foo Street,Bandung,99999,West Java,Indonesia,,99999999,99999999,99999999,Foo,Web Developer,Foo Net,,"It's
+multiline 
+memo."
diff --git a/framework/Data/docs/examples/Yahoo.csv b/framework/Data/docs/examples/Yahoo.csv
new file mode 100644 (file)
index 0000000..43e09dd
--- /dev/null
@@ -0,0 +1,4 @@
+"First","Middle","Last","Nickname","Email","Category","Distribution Lists","Yahoo! ID","Home","Work","Pager","Fax","Mobile","Other","Yahoo! Phone","Primary","Alternate Email 1","Alternate Email 2","Personal Website","Business Website","Title","Company","Work Address","Work City","Work County","Work Post Code","Work Country","Home Address","Home Town","Home County","Home Post Code","Home Country","Birthday","Anniversary","Custom 1","Custom 2","Custom 3","Custom 4","Comments"
+"Firman","","Wandayandi","firman","firman@php.net","","","","","tarzillamax","99999999","","999999999","","","","","","http://php.hm/~firman","","","Foo","Foo Street","Bandung","West Java","99999","Indonesia","Foo Street","Bandung","West Java","99999","Indonesia","","","","","","","It's
+multiline 
+memo."
diff --git a/framework/Data/package.xml b/framework/Data/package.xml
new file mode 100644 (file)
index 0000000..3601f63
--- /dev/null
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Data</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Data API</summary>
+ <description>This package provides a data import and export API, with backends for:
+* CSV
+* TSV
+* iCalendar
+* vCard
+* vNote
+* vTodo
+  
+ </description>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <developer>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </developer>
+ <date>2006-05-08</date>
+ <time>21:12:22</time>
+ <version>
+  <release>0.0.3</release>
+  <api>0.0.3</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="Data">
+    <file baseinstalldir="/Horde" name="csv.php" role="php" />
+    <file baseinstalldir="/Horde" name="icalendar.php" role="php" />
+    <file baseinstalldir="/Horde" name="imc.php" role="php" />
+    <file baseinstalldir="/Horde" name="outlookcsv.php" role="php" />
+    <file baseinstalldir="/Horde" name="tsv.php" role="php" />
+    <file baseinstalldir="/Horde" name="vcard.php" role="php" />
+    <file baseinstalldir="/Horde" name="vnote.php" role="php" />
+    <file baseinstalldir="/Horde" name="vtodo.php" role="php" />
+   </dir> <!-- /Data -->
+   <dir name="tests">
+    <file baseinstalldir="/Horde" name="csv_importFile_01.phpt" role="test" />
+    <file baseinstalldir="/Horde" name="simple_dos.csv" role="test" />
+    <file baseinstalldir="/Horde" name="simple_unix.csv" role="test" />
+   </dir> <!-- /tests -->
+   <file baseinstalldir="/Horde" name="Data.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>iCalendar</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_MIME</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-04-21</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Bugfixes, much better vCard support
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Data/tests/csv_importFile_01.phpt b/framework/Data/tests/csv_importFile_01.phpt
new file mode 100644 (file)
index 0000000..7999b3d
--- /dev/null
@@ -0,0 +1,90 @@
+--TEST--
+Simple CSV files
+--FILE--
+<?php
+
+require 'Horde.php';
+require 'Horde/Data.php';
+
+$data = Horde_Data::factory('csv');
+var_dump($data->importFile(dirname(__FILE__) . '/simple_dos.csv', false, '', '', 4));
+var_dump($data->importFile(dirname(__FILE__) . '/simple_unix.csv', false, '', '', 4));
+var_dump($data->importFile(dirname(__FILE__) . '/simple_dos.csv', true, '', '', 4));
+var_dump($data->importFile(dirname(__FILE__) . '/simple_unix.csv', true, '', '', 4));
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(4) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(10) "three four"
+    [3]=>
+    string(4) "five"
+  }
+  [1]=>
+  array(4) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(5) "seven"
+    [2]=>
+    string(10) "eight nine"
+    [3]=>
+    string(4) " ten"
+  }
+}
+array(2) {
+  [0]=>
+  array(4) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(10) "three four"
+    [3]=>
+    string(4) "five"
+  }
+  [1]=>
+  array(4) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(5) "seven"
+    [2]=>
+    string(10) "eight nine"
+    [3]=>
+    string(4) " ten"
+  }
+}
+array(1) {
+  [0]=>
+  array(4) {
+    ["one"]=>
+    string(3) "six"
+    ["two"]=>
+    string(5) "seven"
+    ["three four"]=>
+    string(10) "eight nine"
+    ["five"]=>
+    string(4) " ten"
+  }
+}
+array(1) {
+  [0]=>
+  array(4) {
+    ["one"]=>
+    string(3) "six"
+    ["two"]=>
+    string(5) "seven"
+    ["three four"]=>
+    string(10) "eight nine"
+    ["five"]=>
+    string(4) " ten"
+  }
+}
\ No newline at end of file
diff --git a/framework/Data/tests/simple_dos.csv b/framework/Data/tests/simple_dos.csv
new file mode 100644 (file)
index 0000000..7cc025d
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three four,five
+six,seven,eight nine, ten
diff --git a/framework/Data/tests/simple_unix.csv b/framework/Data/tests/simple_unix.csv
new file mode 100644 (file)
index 0000000..7cc025d
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three four,five
+six,seven,eight nine, ten
diff --git a/framework/DataTree/DataTree.php b/framework/DataTree/DataTree.php
new file mode 100644 (file)
index 0000000..241d831
--- /dev/null
@@ -0,0 +1,1635 @@
+<?php
+/**
+ * @package Horde_DataTree
+ *
+ * $Horde: framework/DataTree/DataTree.php,v 1.190 2009/06/09 23:23:33 slusarz Exp $
+ */
+
+/** List every object in an array, similar to PEAR/html/menu.php. */
+define('DATATREE_FORMAT_TREE', 1);
+
+/** Get a full list - an array of keys. */
+define('DATATREE_FORMAT_FLAT', 2);
+
+/** The root element (top-level parent) of each DataTree group. */
+define('DATATREE_ROOT', -1);
+
+/** Build a normal select query. */
+define('DATATREE_BUILD_SELECT', 0);
+
+/** Build a count only query. */
+define('DATATREE_BUILD_COUNT', 1);
+
+/** Build an attribute only query. */
+define('DATATREE_BUILD_VALUES', 2);
+
+define('DATATREE_BUILD_VALUES_COUNT', 3);
+
+/**
+ * The DataTree:: class provides a common abstracted interface into the
+ * various backends for the Horde DataTree system.
+ *
+ * A piece of data is just a title that is saved in the page for the null
+ * driver or can be saved in a database to be accessed from everywhere. Every
+ * stored object must have a different name (inside each groupid).
+ *
+ * Required values for $params:<pre>
+ *   'group' -- Define each group of objects we want to build.</pre>
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stephane Huther <shuther1@free.fr>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_DataTree
+ */
+class DataTree {
+
+    /**
+     * Array of all data: indexed by id. The format is:
+     *   array(id => 'name' => name, 'parent' => parent).
+     *
+     * @var array
+     */
+    var $_data = array();
+
+    /**
+     * A hash that can be used to map a full object name
+     * (parent:child:object) to that object's unique ID.
+     *
+     * @var array
+     */
+    var $_nameMap = array();
+
+     /**
+     * Actual attribute sorting hash.
+     *
+     * @var array
+     */
+    var $_sortHash = null;
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *                       We always need 'group', a string that defines the
+     *                       prefix for each set of hierarchical data.
+     */
+    function DataTree($params = array())
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Returns a parameter of this DataTree instance.
+     *
+     * @param string $param  The parameter to return.
+     *
+     * @return mixed  The parameter's value or null if it doesn't exist.
+     */
+    function getParam($param)
+    {
+        return isset($this->_params[$param]) ? $this->_params[$param] : null;
+    }
+
+    /**
+     * Removes an object.
+     *
+     * @param string $object  The object to remove.
+     * @param boolean $force  Force removal of every child object?
+     *
+     * @return TODO
+     */
+    function remove($object, $force = false)
+    {
+        if (is_a($object, 'DataTreeObject')) {
+            $object = $object->getName();
+        }
+
+        if (!$this->exists($object)) {
+            return PEAR::raiseError($object . ' does not exist');
+        }
+
+        $children = $this->getNumberOfChildren($object);
+        if ($children) {
+            /* TODO: remove children if $force == true */
+            return PEAR::raiseError(sprintf(ngettext("Cannot remove, %d child exists.", "Cannot remove, %d children exist.", count($children)), count($children)));
+        }
+
+        $id = $this->getId($object);
+        $pid = $this->getParent($object);
+        $order = $this->_data[$id]['order'];
+        unset($this->_data[$id], $this->_nameMap[$id]);
+
+        // Shift down the order positions.
+        $this->_reorder($pid, $order);
+
+        return $id;
+    }
+
+    /**
+     * Removes all DataTree objects owned by a certain user.
+     *
+     * @abstract
+     *
+     * @param string $user  A user name.
+     *
+     * @return TODO
+     */
+    function removeUserData($user)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Move an object to a new parent.
+     *
+     * @param mixed $object      The object to move.
+     * @param string $newparent  The new parent object. Defaults to the root.
+     *
+     * @return mixed  True on success, PEAR_Error on error.
+     */
+    function move($object, $newparent = null)
+    {
+        $cid = $this->getId($object);
+        if (is_a($cid, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf('Object to move does not exist: %s', $cid->getMessage()));
+        }
+
+        if (!is_null($newparent)) {
+            $pid = $this->getId($newparent);
+            if (is_a($pid, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf('New parent does not exist: %s', $pid->getMessage()));
+            }
+        } else {
+            $pid = DATATREE_ROOT;
+        }
+
+        $this->_data[$cid]['parent'] = $pid;
+
+        return true;
+    }
+
+    /**
+     * Change an object's name.
+     *
+     * @param mixed  $old_object       The old object.
+     * @param string $new_object_name  The new object name.
+     *
+     * @return mixed  True on success, PEAR_Error on error.
+     */
+    function rename($old_object, $new_object_name)
+    {
+        /* Check whether the object exists at all */
+        if (!$this->exists($old_object)) {
+            return PEAR::raiseError($old_object . ' does not exist');
+        }
+
+        /* Check for duplicates - get parent and create new object
+         * name */
+        $parent = $this->getName($this->getParent($old_object));
+        if ($this->exists($parent . ':' . $new_object_name)) {
+            return PEAR::raiseError('Duplicate name ' . $new_object_name);
+        }
+
+        /* Replace the old name with the new one in the cache */
+        $old_object_id = $this->getId($old_object);
+        $this->_data[$old_object_id]['name'] = $new_object_name;
+
+        return true;
+    }
+
+    /**
+     * Changes the order of the children of an object.
+     *
+     * @abstract
+     *
+     * @param string $parent  The full id path of the parent object.
+     * @param mixed $order    If an array it specifies the new positions for
+     *                        all child objects.
+     *                        If an integer and $cid is specified, the position
+     *                        where the child specified by $cid is inserted. If
+     *                        $cid is not specified, the position gets deleted,
+     *                        causing the following positions to shift up.
+     * @param integer $cid    See $order.
+     *
+     * @return TODO
+     */
+    function reorder($parents, $order = null, $cid = null)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Change order of children of an object.
+     *
+     * @param string $pid     The parent object id string path.
+     * @param mixed $order    Specific new order position or an array containing
+     *                        the new positions for the given parent.
+     * @param integer $cid    If provided indicates insertion of a new child to
+     *                        the parent to avoid incrementing it when
+     *                        shifting up all other children's order. If not
+     *                        provided indicates deletion, so shift all other
+     *                        positions down one.
+     */
+    function _reorder($pid, $order = null, $cid = null)
+    {
+        if (!is_array($order) && !is_null($order)) {
+            // Single update (add/del).
+            if (is_null($cid)) {
+                // No id given so shuffle down.
+                foreach ($this->_data as $c_key => $c_val) {
+                    if ($this->_data[$c_key]['parent'] == $pid &&
+                        $this->_data[$c_key]['order'] > $order) {
+                        --$this->_data[$c_key]['order'];
+                    }
+                }
+            } else {
+                // We have an id so shuffle up.
+                foreach ($this->_data as $c_key => $c_val) {
+                    if ($c_key != $cid &&
+                        $this->_data[$c_key]['parent'] == $pid &&
+                        $this->_data[$c_key]['order'] >= $order) {
+                        ++$this->_data[$c_key]['order'];
+                    }
+                }
+            }
+        } elseif (is_array($order) && count($order)) {
+            // Multi update.
+            foreach ($order as $order_position => $cid) {
+                $this->_data[$cid]['order'] = $order_position;
+            }
+        }
+    }
+
+    /**
+     * Explicitly set the order for a datatree object.
+     *
+     * @abstract
+     *
+     * @param integer $id     The datatree object id to change.
+     * @param integer $order  The new order.
+     *
+     * @return TODO
+     */
+    function setOrder($id, $order)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Dynamically determines the object class.
+     *
+     * @param array $attributes  The set of attributes that contain the class
+     *                           information. Defaults to DataTreeObject.
+     *
+     * @return TODO
+     */
+    function _defineObjectClass($attributes)
+    {
+        $class = 'DataTreeObject';
+        if (!is_array($attributes)) {
+            return $class;
+        }
+
+        foreach ($attributes as $attr) {
+            if ($attr['name'] == 'DataTree') {
+                switch ($attr['key']) {
+                case 'objectClass':
+                    $class = $attr['value'];
+                    break;
+
+                case 'objectType':
+                    $result = explode('/', $attr['value']);
+                    $class = $GLOBALS['registry']->callByPackage($result[0], 'defineClass', array('type' => $result[1]));
+                    break;
+                }
+            }
+        }
+
+        return $class;
+    }
+
+    /**
+     * Returns a DataTreeObject (or subclass) object of the data in the
+     * object defined by $object.
+     *
+     * @param string $object  The object to fetch: 'parent:sub-parent:name'.
+     * @param string $class   Subclass of DataTreeObject to use. Defaults to
+     *                        DataTreeObject. Null forces the driver to look
+     *                        into the attributes table to determine the
+     *                        subclass to use. If none is found it uses
+     *                        DataTreeObject.
+     *
+     * @return TODO
+     */
+    function &getObject($object, $class = 'DataTreeObject')
+    {
+        if (empty($object)) {
+            $error = PEAR::raiseError('No object requested.');
+            return $error;
+        }
+
+        $this->_load($object);
+        if (!$this->exists($object)) {
+            $error = PEAR::raiseError($object . ' not found.');
+            return $error;
+        }
+
+        return $this->_getObject($this->getId($object), $object, $class);
+    }
+
+    /**
+     * Returns a DataTreeObject (or subclass) object of the data in the
+     * object with the ID $id.
+     *
+     * @param integer $id    An object id.
+     * @param string $class  Subclass of DataTreeObject to use. Defaults to
+     *                       DataTreeObject. Null forces the driver to look
+     *                       into the attributes table to determine the
+     *                       subclass to use. If none is found it uses
+     *                       DataTreeObject.
+     *
+     * @return TODO
+     */
+    function &getObjectById($id, $class = 'DataTreeObject')
+    {
+        if (empty($id)) {
+            $object = PEAR::raiseError('No id requested.');
+            return $object;
+        }
+
+        $result = $this->_loadById($id);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $this->_getObject($id, $this->getName($id), $class);
+    }
+
+    /**
+     * Helper function for getObject() and getObjectById().
+     *
+     * @access private
+     */
+    function &_getObject($id, $name, $class)
+    {
+        $use_attributes = is_null($class) || is_callable(array($class, '_fromAttributes'));
+        if ($use_attributes) {
+            $attributes = $this->getAttributes($id);
+            if (is_a($attributes, 'PEAR_Error')) {
+                return $attributes;
+            }
+
+            if (is_null($class)) {
+                $class = $this->_defineObjectClass($attributes);
+            }
+        }
+
+        if (!class_exists($class)) {
+            $error = PEAR::raiseError($class . ' not found.');
+            return $error;
+        }
+
+        $dataOb = new $class($name);
+        $dataOb->setDataTree($this);
+
+        /* If the class has a _fromAttributes method, load data from
+         * the attributes backend. */
+        if ($use_attributes) {
+            $dataOb->_fromAttributes($attributes);
+        } else {
+            /* Otherwise load it from the old data storage field. */
+            $dataOb->setData($this->getData($id));
+        }
+
+        $dataOb->setOrder($this->getOrder($name));
+        return $dataOb;
+    }
+
+    /**
+     * Returns an array of DataTreeObject (or subclass) objects
+     * corresponding to the objects in $ids, with the object
+     * names as the keys of the array.
+     *
+     * @param array $ids     An array of object ids.
+     * @param string $class  Subclass of DataTreeObject to use. Defaults to
+     *                       DataTreeObject. Null forces the driver to look
+     *                       into the attributes table to determine the
+     *                       subclass to use. If none is found it uses
+     *                       DataTreeObject.
+     *
+     * @return TODO
+     */
+    function &getObjects($ids, $class = 'DataTreeObject')
+    {
+        $result = $this->_loadById($ids);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $defineClass = is_null($class);
+        $attributes = $defineClass || is_callable(array($class, '_fromAttributes'));
+
+        if ($attributes) {
+            $data = $this->getAttributes($ids);
+        } else {
+            $data = $this->getData($ids);
+        }
+
+        $objects = array();
+        foreach ($ids as $id) {
+            $name = $this->getName($id);
+            if (!empty($name) && !empty($data[$id])) {
+                if ($defineClass) {
+                    $class = $this->_defineObjectClass($data[$id]);
+                }
+
+                if (!class_exists($class)) {
+                    return PEAR::raiseError($class . ' not found.');
+                }
+
+                $objects[$name] = new $class($name);
+                $objects[$name]->setDataTree($this);
+                if ($attributes) {
+                    $objects[$name]->_fromAttributes($data[$id]);
+                } else {
+                    $objects[$name]->setData($data[$id]);
+                }
+                $objects[$name]->setOrder($this->getOrder($name));
+            }
+        }
+
+        return $objects;
+    }
+
+    /**
+     * Export a list of objects.
+     *
+     * @param constant $format       Format of the export
+     * @param string   $startleaf    The name of the leaf from which we start
+     *                               the export tree.
+     * @param boolean  $reload       Re-load the requested chunk? Defaults to
+     *                               false (only what is currently loaded).
+     * @param string   $rootname     The label to use for the root element.
+     *                               Defaults to DATATREE_ROOT.
+     * @param integer  $maxdepth     The maximum number of levels to return.
+     *                               Defaults to DATATREE_ROOT, which is no
+     *                               limit.
+     * @param boolean  $loadTree     Load a tree starting at $root, or just the
+     *                               requested level and direct parents?
+     *                               Defaults to single level.
+     * @param string   $sortby_name  Attribute name to use for sorting.
+     * @param string   $sortby_key   Attribute key to use for sorting.
+     * @param integer  $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return mixed  The tree representation of the objects, or a PEAR_Error
+     *                on failure.
+     */
+    function get($format, $startleaf = DATATREE_ROOT, $reload = false,
+                 $rootname = DATATREE_ROOT, $maxdepth = -1, $loadTree = false,
+                 $sortby_name = null, $sortby_key = null, $direction = 0)
+    {
+        $out = array();
+
+        /* Set sorting hash */
+        if (!is_null($sortby_name)) {
+            $this->_sortHash = DataTree::sortHash($startleaf, $sortby_name, $sortby_key, $direction);
+        }
+
+        $this->_load($startleaf, $loadTree, $reload, $sortby_name, $sortby_key, $direction);
+
+        switch ($format) {
+        case DATATREE_FORMAT_TREE:
+            $startid = $this->getId($startleaf, $maxdepth);
+            if (is_a($startid, 'PEAR_Error')) {
+                return $startid;
+            }
+            $this->_extractAllLevelTree($out, $startid, $maxdepth);
+            break;
+
+        case DATATREE_FORMAT_FLAT:
+            $startid = $this->getId($startleaf);
+            if (is_a($startid, 'PEAR_Error')) {
+                return $startid;
+            }
+            $this->_extractAllLevelList($out, $startid, $maxdepth);
+            if (!empty($out[DATATREE_ROOT])) {
+                $out[DATATREE_ROOT] = $rootname;
+            }
+            break;
+
+        default:
+            return PEAR::raiseError('Not supported');
+        }
+
+        if (!is_null($this->_sortHash)) {
+            /* Reset sorting hash. */
+            $this->_sortHash = null;
+
+            /* Reverse since the attribute sorting combined with tree up-ward
+             * sorting produces a reversed object order. */
+            $out = array_reverse($out, true);
+        }
+
+        return $out;
+    }
+
+    /**
+     * Counts objects.
+     *
+     * @param string $startleaf  The name of the leaf from which we start
+     *                           counting.
+     *
+     * @return integer  The number of the objects below $startleaf.
+     */
+    function count($startleaf = DATATREE_ROOT)
+    {
+        return $this->_count($startleaf);
+    }
+
+    /**
+     * Create attribute sort hash
+     *
+     * @since Horde 3.1
+     *
+     * @param string  $root         The name of the leaf from which we start
+     *                              the export tree.
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                              0 - ascending
+     *                              1 - descending
+     *
+     * @return string  The sort hash.
+     */
+    function sortHash($root, $sortby_name = null, $sortby_key = null,
+                      $direction = 0)
+    {
+        return sprintf('%s-%s-%s-%s', $root, $sortby_name, $sortby_key, $direction);
+    }
+
+    /**
+     * Export a list of objects just like get() above, but uses an
+     * object id to fetch the list of objects.
+     *
+     * @param constant $format    Format of the export.
+     * @param string  $startleaf  The id of the leaf from which we start the
+     *                            export tree.
+     * @param boolean $reload     Reload the requested chunk? Defaults to
+     *                            false (only what is currently loaded).
+     * @param string  $rootname   The label to use for the root element.
+     *                            Defaults to DATATREE_ROOT.
+     * @param integer $maxdepth   The maximum number of levels to return
+     *                            Defaults to -1, which is no limit.
+     *
+     * @return mixed  The tree representation of the objects, or a PEAR_Error
+     *                on failure.
+     */
+    function getById($format, $startleaf = DATATREE_ROOT, $reload = false,
+                     $rootname = DATATREE_ROOT, $maxdepth = -1)
+    {
+        $this->_loadById($startleaf);
+        $out = array();
+
+        switch ($format) {
+        case DATATREE_FORMAT_TREE:
+            $this->_extractAllLevelTree($out, $startleaf, $maxdepth);
+            break;
+
+        case DATATREE_FORMAT_FLAT:
+            $this->_extractAllLevelList($out, $startleaf, $maxdepth);
+            if (!empty($out[DATATREE_ROOT])) {
+                $out[DATATREE_ROOT] = $rootname;
+            }
+            break;
+
+        default:
+            return PEAR::raiseError('Not supported');
+        }
+
+        return $out;
+    }
+
+    /**
+     * Returns a list of all groups (root nodes) of the data tree.
+     *
+     * @abstract
+     *
+     * @return mixed  The group IDs or PEAR_Error on error.
+     */
+    function getGroups()
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Retrieve data for an object from the datatree_data field.
+     *
+     * @abstract
+     *
+     * @param integer $cid  The object id to fetch, or an array of object ids.
+     *
+     * @return TODO
+     */
+    function getData($cid)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Import a list of objects. Used by drivers to populate the internal
+     * $_data array.
+     *
+     * @param array $data      The data to import.
+     * @param string $charset  The charset to convert the object name from.
+     *
+     * @return TODO
+     */
+    function set($data, $charset = null)
+    {
+        $cids = array();
+        foreach ($data as $id => $cat) {
+            if (!is_null($charset)) {
+                $cat[1] = Horde_String::convertCharset($cat[1], $charset);
+            }
+            $cids[$cat[0]] = $cat[1];
+            $cparents[$cat[0]] = $cat[2];
+            $corders[$cat[0]] = $cat[3];
+            $sorders[$cat[0]] = $id;
+        }
+
+        foreach ($cids as $id => $name) {
+            $this->_data[$id]['name'] = $name;
+            $this->_data[$id]['order'] = $corders[$id];
+            if (!is_null($this->_sortHash)) {
+                $this->_data[$id]['sorter'][$this->_sortHash] = $sorders[$id];
+            }
+            if (!empty($cparents[$id])) {
+                $parents = explode(':', substr($cparents[$id], 1));
+                $par = $parents[count($parents) - 1];
+                $this->_data[$id]['parent'] = $par;
+
+                if (!empty($this->_nameMap[$par])) {
+                    // If we've already loaded the direct parent of
+                    // this object, use that to find the full name.
+                    $this->_nameMap[$id] = $this->_nameMap[$par] . ':' . $name;
+                } else {
+                    // Otherwise, run through parents one by one to
+                    // build it up.
+                    $this->_nameMap[$id] = '';
+                    foreach ($parents as $parID) {
+                        if (!empty($cids[$parID])) {
+                            $this->_nameMap[$id] .= ':' . $cids[$parID];
+                        }
+                    }
+                    $this->_nameMap[$id] = substr($this->_nameMap[$id], 1) . ':' . $name;
+                }
+            } else {
+                $this->_data[$id]['parent'] = DATATREE_ROOT;
+                $this->_nameMap[$id] = $name;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Extract one level of data for a parent leaf, sorted first by
+     * their order and then by name. This function is a way to get a
+     * collection of $leaf's children.
+     *
+     * @param string $leaf  Name of the parent from which to start.
+     *
+     * @return array  TODO
+     */
+    function _extractOneLevel($leaf = DATATREE_ROOT)
+    {
+        $out = array();
+        foreach ($this->_data as $id => $vals) {
+            if ($vals['parent'] == $leaf) {
+                $out[$id] = $vals;
+            }
+        }
+
+        uasort($out, array($this, (is_null($this->_sortHash)) ? '_cmp' : '_cmpSorted'));
+
+        return $out;
+    }
+
+    /**
+     * Extract all levels of data, starting from a given parent
+     * leaf in the datatree.
+     *
+     * @access private
+     *
+     * @note If nothing is returned that means there is no child, but
+     * don't forget to add the parent if any subsequent operations are
+     * required!
+     *
+     * @param array $out         This is an iterating function, so $out is
+     *                           passed by reference to contain the result.
+     * @param string $parent     The name of the parent from which to begin.
+     * @param integer $maxdepth  Max of levels of depth to check.
+     *
+     * @return TODO
+     */
+    function _extractAllLevelTree(&$out, $parent = DATATREE_ROOT,
+                                  $maxdepth = -1)
+    {
+        if ($maxdepth == 0) {
+            return false;
+        }
+
+        $out[$parent] = true;
+
+        $k = $this->_extractOneLevel($parent);
+        foreach (array_keys($k) as $object) {
+            if (!is_array($out[$parent])) {
+                $out[$parent] = array();
+            }
+            $out[$parent][$object] = true;
+            $this->_extractAllLevelTree($out[$parent], $object, $maxdepth - 1);
+        }
+    }
+
+    /**
+     * Extract all levels of data, starting from any parent in
+     * the tree.
+     *
+     * Returned array format: array(parent => array(child => true))
+     *
+     * @access private
+     *
+     * @param array $out         This is an iterating function, so $out is
+     *                           passed by reference to contain the result.
+     * @param string $parent     The name of the parent from which to begin.
+     * @param integer $maxdepth  Max number of levels of depth to check.
+     *
+     * @return TODO
+     */
+    function _extractAllLevelList(&$out, $parent = DATATREE_ROOT,
+                                  $maxdepth = -1)
+    {
+        if ($maxdepth == 0) {
+            return false;
+        }
+
+        // This is redundant most of the time, so make sure we need to
+        // do it.
+        if (empty($out[$parent])) {
+            $out[$parent] = $this->getName($parent);
+        }
+
+        foreach (array_keys($this->_extractOneLevel($parent)) as $object) {
+            $out[$object] = $this->getName($object);
+            $this->_extractAllLevelList($out, $object, $maxdepth - 1);
+        }
+    }
+
+    /**
+     * Returns a child's direct parent ID.
+     *
+     * @param mixed $child  Either the object, an array containing the
+     *                      path elements, or the object name for which
+     *                      to look up the parent's ID.
+     *
+     * @return mixed  The unique ID of the parent or PEAR_Error on error.
+     */
+    function getParent($child)
+    {
+        if (is_a($child, 'DataTreeObject')) {
+            $child = $child->getName();
+        }
+        $id = $this->getId($child);
+        if (is_a($id, 'PEAR_Error')) {
+            return $id;
+        }
+        return $this->getParentById($id);
+    }
+
+    /**
+     * Get a $child's direct parent ID.
+     *
+     * @param integer $childId  Get the parent of this object.
+     *
+     * @return mixed  The unique ID of the parent or PEAR_Error on error.
+     */
+    function getParentById($childId)
+    {
+        $this->_loadById($childId);
+        return isset($this->_data[$childId]) ?
+            $this->_data[$childId]['parent'] :
+            PEAR::raiseError($childId . ' not found');
+    }
+
+    /**
+     * Get a list of parents all the way up to the root object for
+     * $child.
+     *
+     * @param mixed $child     The name of the child
+     * @param boolean $getids  If true, return parent IDs; otherwise, return
+     *                         names.
+     *
+     * @return mixed  [child] [parent] in a tree format or PEAR_Error.
+     */
+    function getParents($child, $getids = false)
+    {
+        $pid = $this->getParent($child);
+        if (is_a($pid, 'PEAR_Error')) {
+            return PEAR::raiseError('Parents not found: ' . $pid->getMessage());
+        }
+        $pname = $this->getName($pid);
+        $parents = ($getids) ? array($pid => true) : array($pname => true);
+
+        if ($pid != DATATREE_ROOT) {
+            if ($getids) {
+                $parents[$pid] = $this->getParents($pname, $getids);
+            } else {
+                $parents[$pname] = $this->getParents($pname, $getids);
+            }
+        }
+
+        return $parents;
+    }
+
+    /**
+     * Get a list of parents all the way up to the root object for
+     * $child.
+     *
+     * @param integer $childId  The id of the child.
+     * @param array $parents    The array, as we build it up.
+     *
+     * @return array  A flat list of all of the parents of $child,
+     *                hashed in $id => $name format.
+     */
+    function getParentList($childId, $parents = array())
+    {
+        $pid = $this->getParentById($childId);
+        if (is_a($pid, 'PEAR_Error')) {
+            return PEAR::raiseError('Parents not found: ' . $pid->getMessage());
+        }
+
+        if ($pid != DATATREE_ROOT) {
+            $parents[$pid] = $this->getName($pid);
+            $parents = $this->getParentList($pid, $parents);
+        }
+
+        return $parents;
+    }
+
+    /**
+     * Get a parent ID string (id:cid format) for the specified object.
+     *
+     * @param mixed $object  The object to return a parent string for.
+     *
+     * @return string|PEAR_Error  The ID "path" to the parent object or
+     *                            PEAR_Error on failure.
+     */
+    function getParentIdString($object)
+    {
+        $ptree = $this->getParents($object, true);
+        if (is_a($ptree, 'PEAR_Error')) {
+            return $ptree;
+        }
+
+        $pids = '';
+        while ((list($id, $parent) = each($ptree)) && is_array($parent)) {
+            $pids = ':' . $id . $pids;
+            $ptree = $parent;
+        }
+
+        return $pids;
+    }
+
+    /**
+     * Get the number of children an object has, only counting immediate
+     * children, not grandchildren, etc.
+     *
+     * @param mixed $parent  Either the object or the name for which to count
+     *                       the children, defaults to the root
+     *                       (DATATREE_ROOT).
+     *
+     * @return integer
+     */
+    function getNumberOfChildren($parent = DATATREE_ROOT)
+    {
+        if (is_a($parent, 'DataTreeObject')) {
+            $parent = $parent->getName();
+        }
+        $this->_load($parent);
+        $out = $this->_extractOneLevel($this->getId($parent));
+
+        return is_array($out) ? count($out) : 0;
+    }
+
+    /**
+     * Check if an object exists or not. The root element DATATREE_ROOT always
+     * exists.
+     *
+     * @param mixed $object  The name of the object.
+     *
+     * @return boolean  True if the object exists, false otherwise.
+     */
+    function exists($object)
+    {
+        if (empty($object)) {
+            return false;
+        }
+
+        if (is_a($object, 'DataTreeObject')) {
+            $object = $object->getName();
+        } elseif (is_array($object)) {
+            $object = implode(':', $object);
+        }
+
+        if ($object == DATATREE_ROOT) {
+            return true;
+        }
+
+        if (array_search($object, $this->_nameMap) !== false) {
+            return true;
+        }
+
+        // Consult the backend directly.
+        return $this->_exists($object);
+    }
+
+    /**
+     * Get the name of an object from its id.
+     *
+     * @param integer $id  The id for which to look up the name.
+     *
+     * @return string  TODO
+     */
+    function getName($id)
+    {
+        /* If no id or if id is a PEAR error, return null. */
+        if (empty($id) || is_a($id, 'PEAR_Error')) {
+            return null;
+        }
+
+        /* If checking name of root, return DATATREE_ROOT. */
+        if ($id == DATATREE_ROOT) {
+            return DATATREE_ROOT;
+        }
+
+        /* If found in the name map, return the name. */
+        if (isset($this->_nameMap[$id])) {
+            return $this->_nameMap[$id];
+        }
+
+        /* Not found in name map, consult the backend. */
+        return $this->_getName($id);
+    }
+
+    /**
+     * Get the id of an object from its name.
+     *
+     * @param mixed $name  Either the object, an array containing the
+     *                     path elements, or the object name for which
+     *                     to look up the id.
+     *
+     * @return string
+     */
+    function getId($name)
+    {
+        /* Check if $name is not a string. */
+        if (is_a($name, 'DataTreeObject')) {
+            /* DataTreeObject, get the string name. */
+            $name = $name->getName();
+        } elseif (is_array($name)) {
+            /* Path array, implode to get the string name. */
+            $name = implode(':', $name);
+        }
+
+        /* If checking id of root, return DATATREE_ROOT. */
+        if ($name == DATATREE_ROOT) {
+            return DATATREE_ROOT;
+        }
+
+        /* Flip the name map to look up the id using the name as key. */
+        if (($id = array_search($name, $this->_nameMap)) !== false) {
+            return $id;
+        }
+
+        /* Not found in name map, consult the backend. */
+        $id = $this->_getId($name);
+        if (is_null($id)) {
+            return PEAR::raiseError($name . ' does not exist');
+        }
+        return $id;
+    }
+
+    /**
+     * Get the order position of an object.
+     *
+     * @param mixed $child  Either the object or the name.
+     *
+     * @return mixed  The object's order position or a PEAR error on failure.
+     */
+    function getOrder($child)
+    {
+        if (is_a($child, 'DataTreeObject')) {
+            $child = $child->getName();
+        }
+        $id = $this->getId($child);
+        if (is_a($id, 'PEAR_Error')) {
+            return $id;
+        }
+        $this->_loadById($id);
+
+        return isset($this->_data[$id]['order']) ?
+            $this->_data[$id]['order'] :
+            null;
+    }
+
+    /**
+     * Replace all occurences of ':' in an object name with '.'.
+     *
+     * @param string $name  The name of the object.
+     *
+     * @return string  The encoded name.
+     */
+    function encodeName($name)
+    {
+        return str_replace(':', '.', $name);
+    }
+
+    /**
+     * Get the short name of an object, returns only the last portion of the
+     * full name. For display purposes only.
+     *
+     * @static
+     *
+     * @param string $name  The name of the object.
+     *
+     * @return string  The object's short name.
+     */
+    function getShortName($name)
+    {
+        /* If there are several components to the name, explode and get the
+         * last one, otherwise just return the name. */
+        if (strpos($name, ':') !== false) {
+            $name = explode(':', $name);
+            $name = array_pop($name);
+        }
+        return $name;
+    }
+
+    /**
+     * Returns a tree sorted by the specified attribute name and/or key.
+     *
+     * @abstract
+     *
+     * @since Horde 3.1
+     *
+     * @param string $root         Which portion of the tree to sort.
+     *                             Defaults to all of it.
+     * @param boolean $loadTree    Sort the tree starting at $root, or just the
+     *                             requested level and direct parents?
+     *                             Defaults to single level.
+     * @param string $sortby_name  Attribute name to use for sorting.
+     * @param string $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction   Sort direction:
+     *                             0 - ascending
+     *                             1 - descending
+     *
+     * @return array TODO
+     */
+    function getSortedTree($root, $loadTree = false, $sortby_name = null,
+                           $sortby_key = null, $direction = 0)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Adds an object.
+     *
+     * @abstract
+     *
+     * @param mixed $object        The object to add (string or
+     *                             DataTreeObject).
+     * @param boolean $id_as_name  True or false to indicate if object ID is to
+     *                             be used as object name. Used in situations
+     *                             where there is no available unique input for
+     *                             object name.
+     *
+     * @return TODO
+     */
+    function add($object, $id_as_name = false)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Add an object.
+     *
+     * @private
+     *
+     * @param string   $name  The short object name.
+     * @param integer  $id    The new object's unique ID.
+     * @param integer  $pid   The unique ID of the object's parent.
+     * @param integer  $order The ordering data for the object.
+     *
+     * @access protected
+     *
+     * @return TODO
+     */
+    function _add($name, $id, $pid, $order = '')
+    {
+        $this->_data[$id] = array('name' => $name,
+                                  'parent' => $pid,
+                                  'order' => $order);
+        $this->_nameMap[$id] = $name;
+
+        /* Shift along the order positions. */
+        $this->_reorder($pid, $order, $id);
+
+        return true;
+    }
+
+    /**
+     * Retrieve data for an object from the horde_datatree_attributes
+     * table.
+     *
+     * @abstract
+     *
+     * @param integer | array $cid  The object id to fetch,
+     *                              or an array of object ids.
+     *
+     * @return array  A hash of attributes, or a multi-level hash
+     *                of object ids => their attributes.
+     */
+    function getAttributes($cid)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Returns the number of objects matching a set of attribute criteria.
+     *
+     * @abstract
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria   The array of criteria.
+     * @param string  $parent     The parent node to start searching from.
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent? Defaults to all levels.
+     * @param string  $restrict   Only return attributes with the same
+     *                            attribute_name or attribute_id.
+     *
+     * @return TODO
+     */
+    function countByAttributes($criteria, $parent = DATATREE_ROOT,
+                               $allLevels = true, $restrict = 'name')
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Returns a set of object ids based on a set of attribute criteria.
+     *
+     * @abstract
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param integer $from         The object to start to fetching
+     * @param integer $count        The number of objects to fetch
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return TODO
+     */
+    function getByAttributes($criteria, $parent = DATATREE_ROOT,
+                             $allLevels = true, $restrict = 'name', $from = 0,
+                             $count = 0, $sortby_name = null,
+                             $sortby_key = null, $direction = 0)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Sorts IDs by attribute values. IDs without attributes will be added to
+     * the end of the sorted list.
+     *
+     * @abstract
+     *
+     * @param array $unordered_ids  Array of ids to sort.
+     * @param array $sortby_name    Attribute name to use for sorting.
+     * @param array $sortby_key     Attribute key to use for sorting.
+     * @param array $direction      Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return array  Sorted ids.
+     */
+    function sortByAttributes($unordered_ids, $sortby_name = null,
+                              $sortby_key = null, $direction = 0)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Update the data in an object. Does not change the object's
+     * parent or name, just serialized data or attributes.
+     *
+     * @abstract
+     *
+     * @param DataTree $object  A DataTree object.
+     *
+     * @return TODO
+     */
+    function updateData($object)
+    {
+        return PEAR::raiseError('not supported');
+    }
+
+    /**
+     * Sort two objects by their order field, and if that is the same,
+     * alphabetically (case insensitive) by name.
+     *
+     * You never call this function; it's used in uasort() calls. Do
+     * NOT use usort(); you'll lose key => value associations.
+     *
+     * @private
+     *
+     * @param array $a  The first object
+     * @param array $b  The second object
+     *
+     * @return integer  1 if $a should be first,
+     *                 -1 if $b should be first,
+     *                  0 if they are entirely equal.
+     */
+    function _cmp($a, $b)
+    {
+        if ($a['order'] > $b['order']) {
+            return 1;
+        } elseif ($a['order'] < $b['order']) {
+            return -1;
+        } else {
+            return strcasecmp($a['name'], $b['name']);
+        }
+    }
+
+     /**
+     * Sorts two objects by their sorter hash field.
+     *
+     * You never call this function; it's used in uasort() calls. Do NOT use
+     * usort(); you'll lose key => value associations.
+     *
+     * @since Horde 3.1
+     *
+     * @private
+     *
+     * @param array $a  The first object
+     * @param array $b  The second object
+     *
+     * @return integer  1 if $a should be first,
+     *                 -1 if $b should be first,
+     *                  0 if they are entirely equal.
+     */
+    function _cmpSorted($a, $b)
+    {
+        return intval($a['sorter'][$this->_sortHash] < $b['sorter'][$this->_sortHash]);
+    }
+
+    /**
+     * Attempts to return a concrete DataTree instance based on $driver.
+     *
+     * @param mixed $driver  The type of concrete DataTree subclass to return.
+     *                       This is based on the storage driver ($driver). The
+     *                       code is dynamically included. If $driver is an array,
+     *                       then we will look in $driver[0]/lib/DataTree/ for
+     *                       the subclass implementation named $driver[1].php.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *                       Here, we need 'group' = a string that defines
+     *                       top-level groups of objects.
+     *
+     * @return DataTree  The newly created concrete DataTree instance, or false
+     *                   on an error.
+     */
+    function &factory($driver, $params = null)
+    {
+        $driver = basename($driver);
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('datatree', $driver);
+        }
+
+        if (empty($driver)) {
+            $driver = 'null';
+        }
+
+        include_once 'Horde/DataTree/' . $driver . '.php';
+        $class = 'DataTree_' . $driver;
+        if (class_exists($class)) {
+            $dt = new $class($params);
+            $result = $dt->_init();
+            if (is_a($result, 'PEAR_Error')) {
+                include_once 'Horde/DataTree/null.php';
+                $dt = new DataTree_null($params);
+            }
+        } else {
+            $dt = PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+
+        return $dt;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete DataTree instance based on
+     * $driver.
+     *
+     * It will only create a new instance if no DataTree instance with the same
+     * parameters currently exists.
+     *
+     * This should be used if multiple DataTree sources (and, thus, multiple
+     * DataTree instances) are required.
+     *
+     * This method must be invoked as: $var = &DataTree::singleton();
+     *
+     * @param mixed $driver  Type of concrete DataTree subclass to return,
+     *                       based on storage driver ($driver). The code is
+     *                       dynamically included. If $driver is an array, then
+     *                       look in $driver[0]/lib/DataTree/ for subclass
+     *                       implementation named $driver[1].php.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return DataTree  The concrete DataTree reference, or false on an error.
+     */
+    function &singleton($driver, $params = null)
+    {
+        static $instances = array();
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('datatree', $driver);
+        }
+
+        $signature = serialize(array($driver, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &DataTree::factory($driver, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
+
+/**
+ * Class that can be extended to save arbitrary information as part of a stored
+ * object.
+ *
+ * @author  Stephane Huther <shuther1@free.fr>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 2.1
+ * @package Horde_DataTree
+ */
+class DataTreeObject {
+
+    /**
+     * This object's DataTree instance.
+     *
+     * @var DataTree
+     */
+    var $datatree;
+
+    /**
+     * Key-value hash that will be serialized.
+     *
+     * @see getData()
+     * @var array
+     */
+    var $data = array();
+
+    /**
+     * The unique name of this object.
+     * These names have the same requirements as other object names - they must
+     * be unique, etc.
+     *
+     * @var string
+     */
+    var $name;
+
+    /**
+     * If this object has ordering data, store it here.
+     *
+     * @var integer
+     */
+    var $order = null;
+
+    /**
+     * DataTreeObject constructor.
+     * Just sets the $name parameter.
+     *
+     * @param string $name  The object name.
+     */
+    function DataTreeObject($name)
+    {
+        $this->setName($name);
+    }
+
+    /**
+     * Sets the {@link DataTree} instance used to retrieve this object.
+     *
+     * @param DataTree $datatree  A {@link DataTree} instance.
+     */
+    function setDataTree(&$datatree)
+    {
+        $this->datatree = &$datatree;
+    }
+
+    /**
+     * Gets the name of this object.
+     *
+     * @return string The object name.
+     */
+    function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Sets the name of this object.
+     *
+     * NOTE: Use with caution. This may throw out of sync the cached datatree
+     * tables if not used properly.
+     *
+     * @param string $name  The name to set this object's name to.
+     */
+    function setName($name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Gets the short name of this object.
+     * For display purposes only.
+     *
+     * @return string  The object's short name.
+     */
+    function getShortName()
+    {
+        return DataTree::getShortName($this->name);
+    }
+
+    /**
+     * Gets the ID of this object.
+     *
+     * @return string  The object's ID.
+     */
+    function getId()
+    {
+        return $this->datatree->getId($this);
+    }
+
+    /**
+     * Gets the data array.
+     *
+     * @return array  The internal data array.
+     */
+    function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Sets the data array.
+     *
+     * @param array  The data array to store internally.
+     */
+    function setData($data)
+    {
+        $this->data = $data;
+    }
+
+    /**
+     * Sets the order of this object in its object collection.
+     *
+     * @param integer $order
+     */
+    function setOrder($order)
+    {
+        $this->order = $order;
+    }
+
+    /**
+     * Returns this object's parent.
+     *
+     * @param string $class   Subclass of DataTreeObject to use. Defaults to
+     *                        DataTreeObject. Null forces the driver to look
+     *                        into the attributes table to determine the
+     *                        subclass to use. If none is found it uses
+     *                        DataTreeObject.
+     *
+     * @return DataTreeObject  This object's parent
+     */
+    function &getParent($class = 'DataTreeObject')
+    {
+        $id = $this->datatree->getParent($this);
+        if (is_a($id, 'PEAR_Error')) {
+            return $id;
+        }
+        return $this->datatree->getObjectById($id, $class);
+    }
+
+    /**
+     * Returns a child of this object.
+     *
+     * @param string $name         The child's name.
+     * @param boolean $autocreate  If true and no child with the given name
+     *                             exists, one gets created.
+     */
+    function &getChild($name, $autocreate = true)
+    {
+        $name = $this->getShortName() . ':' . $name;
+
+        /* If the child shouldn't get created, we don't check for its
+         * existance to return the "not found" error of
+         * getObject(). */
+        if (!$autocreate || $this->datatree->exists($name)) {
+            $child = &$this->datatree->getObject($name);
+        } else {
+            $child = new DataTreeObject($name);
+            $child->setDataTree($this->datatree);
+            $this->datatree->add($child);
+        }
+
+        return $child;
+    }
+
+    /**
+     * Saves any changes to this object to the backend permanently. New objects
+     * are added instead.
+     *
+     * @return boolean|PEAR_Error  PEAR_Error on failure.
+     */
+    function save()
+    {
+        if ($this->datatree->exists($this)) {
+            return $this->datatree->updateData($this);
+        } else {
+            return $this->datatree->add($this);
+        }
+    }
+
+    /**
+     * Delete this object from the backend permanently.
+     *
+     * @return boolean|PEAR_Error  PEAR_Error on failure.
+     */
+    function delete()
+    {
+        return $this->datatree->remove($this);
+    }
+
+    /**
+     * Gets one of the attributes of the object, or null if it isn't defined.
+     *
+     * @param string $attribute  The attribute to get.
+     *
+     * @return mixed  The value of the attribute, or null.
+     */
+    function get($attribute)
+    {
+        return isset($this->data[$attribute])
+            ? $this->data[$attribute]
+            : null;
+    }
+
+    /**
+     * Sets one of the attributes of the object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     */
+    function set($attribute, $value)
+    {
+        $this->data[$attribute] = $value;
+    }
+
+}
diff --git a/framework/DataTree/DataTree/null.php b/framework/DataTree/DataTree/null.php
new file mode 100644 (file)
index 0000000..729f001
--- /dev/null
@@ -0,0 +1,417 @@
+<?php
+/**
+ * The DataTree_null:: class provides a dummy implementation of the
+ * DataTree:: API; no data will last beyond a single page request.
+ *
+ * $Horde: framework/DataTree/DataTree/null.php,v 1.37 2009/01/06 17:49:14 jan Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stephane Huther <shuther1@free.fr>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_DataTree
+ */
+class DataTree_null extends DataTree {
+
+    /**
+     * Cache of attributes for any objects created during this page request.
+     *
+     * @var array
+     */
+    var $_attributeCache = array();
+
+    /**
+     * Cache of data for any objects created during this page request.
+     *
+     * @var array
+     */
+    var $_dataCache = array();
+
+    /**
+     * Load (a subset of) the datatree into the $_data array. Part of the
+     * DataTree API that must be overridden by subclasses.
+     *
+     * @param string  $root    Which portion of the tree to load. Defaults to
+     *                         all of it.
+     * @param boolean $reload  Re-load already loaded values?
+     *
+     * @return mixed  True on success or a PEAR_Error on failure.
+     *
+     * @access private
+     */
+    function _load($root = null, $reload = false)
+    {
+    }
+
+    /**
+     * Load a specific object identified by its unique ID ($id), and
+     * its parents, into the $_data array.
+     *
+     * @param integer $cid  The unique ID of the object to load.
+     *
+     * @return mixed  True on success or a PEAR_Error on failure.
+     *
+     * @access private
+     */
+    function _loadById($cid)
+    {
+    }
+
+    /**
+     * Check for existance of an object in a backend-specific manner.
+     *
+     * @param string $object_name Object name to check for.
+     *
+     * @return boolean True if the object exists, false otherwise.
+     */
+    function _exists($object_name)
+    {
+        return false;
+    }
+
+    /**
+     * Look up a datatree id by name.
+     *
+     * @param string $name
+     *
+     * @return integer DataTree id
+     */
+    function _getId($name)
+    {
+        return null;
+    }
+
+    /**
+     * Look up a datatree name by id.
+     *
+     * @param integer $id
+     *
+     * @return string DataTree name
+     */
+    function _getName($id)
+    {
+        return null;
+    }
+
+    /**
+     * Get a tree sorted by the specified attribute name and/or key.
+     *
+     * @since Horde 3.1
+     *
+     * @param string  $root       Which portion of the tree to sort.
+     *                            Defaults to all of it.
+     * @param boolean $loadTree   Sort the tree starting at $root, or just the
+     *                            requested level and direct parents?
+     *                            Defaults to single level.
+     * @param array $sortby_name  Attribute name to use for sorting.
+     * @param array $sortby_key   Attribute key to use for sorting.
+     * @param array $direction    Sort direction:
+     *                              0 - ascending
+     *                              1 - descending
+     *
+     * @return array TODO
+     */
+    function getSortedTree($root, $loadTree = false, $sortby_name = null, $sortby_key = null, $direction = 0)
+    {
+        return array();
+    }
+
+    /**
+     * Add an object. Part of the DataTree API that must be
+     * overridden by subclasses.
+     *
+     * @param mixed $fullname  The object to add (string or DataTreeObject).
+     */
+    function add($object)
+    {
+        if (is_a($object, 'DataTreeObject')) {
+            $fullname = $object->getName();
+            $order = $object->order;
+        } else {
+            $fullname = $object;
+            $order = null;
+        }
+
+        $id = md5(mt_rand());
+        if (strpos($fullname, ':') !== false) {
+            $parts = explode(':', $fullname);
+            $name = array_pop($parts);
+            $parent = implode(':', $parts);
+            $pid = $this->getId($parent);
+            if (is_a($pid, 'PEAR_Error')) {
+                $this->add($parent);
+            }
+        } else {
+            $pid = DATATREE_ROOT;
+        }
+
+        if (parent::exists($fullname)) {
+            return PEAR::raiseError('Already exists');
+        }
+
+        $added = parent::_add($fullname, $id, $pid, $order);
+        if (is_a($added, 'PEAR_Error')) {
+            return $added;
+        }
+        return $this->updateData($object);
+    }
+
+    /**
+     * Change order of the children of an object.
+     *
+     * @param string $parents  The parent id string path.
+     * @param mixed $order     A specific new order position or an array
+     *                         containing the new positions for the given
+     *                         $parents object.
+     * @param integer $cid     If provided indicates insertion of a new child
+     *                         to the object, and will be used to avoid
+     *                         incrementing it when shifting up all other
+     *                         children's order. If not provided indicates
+     *                         deletion, hence shift all other positions down
+     *                         one.
+     */
+    function reorder($parents, $order = null, $cid = null)
+    {
+        if (is_array($order) && !empty($order)) {
+            // Multi update.
+            $this->_reorder($pid, $order);
+        }
+    }
+
+    /**
+     * Explicitly set the order for a datatree object.
+     *
+     * @param integer $id     The datatree object id to change.
+     * @param integer $order  The new order.
+     */
+    function setOrder($id, $order)
+    {
+    }
+
+    /**
+     * Removes an object.
+     *
+     * @param mixed   $object  The object to remove.
+     * @param boolean $force   Force removal of every child object?
+     */
+    function remove($object, $force = false)
+    {
+    }
+
+    /**
+     * Remove one or more objects by id. This function does *not* do
+     * the validation, reordering, etc. that remove() does. If you
+     * need to check for children, re-do ordering, etc., then you must
+     * remove() objects one-by-one. This is for code that knows it's
+     * dealing with single (non-parented) objects and needs to delete
+     * a batch of them quickly.
+     *
+     * @param array $ids  The objects to remove.
+     */
+    function removeByIds($ids)
+    {
+    }
+
+    /**
+     * Remove one or more objects by name. This function does *not* do
+     * the validation, reordering, etc. that remove() does. If you
+     * need to check for children, re-do ordering, etc., then you must
+     * remove() objects one-by-one. This is for code that knows it's
+     * dealing with single (non-parented) objects and needs to delete
+     * a batch of them quickly.
+     *
+     * @param array $names  The objects to remove.
+     */
+    function removeByNames($names)
+    {
+    }
+
+    /**
+     * Move an object to a new parent.
+     *
+     * @param mixed  $object     The object to move.
+     * @param string $newparent  The new parent object. Defaults to the root.
+     */
+    function move($object, $newparent = null)
+    {
+    }
+
+    /**
+     * Change an object's name.
+     *
+     * @param mixed  $old_object       The old object.
+     * @param string $new_object_name  The new object name.
+     */
+    function rename($old_object, $new_object_name)
+    {
+    }
+
+    /**
+     * Retrieve data for an object from the datatree_data field.
+     *
+     * @param integer $cid  The object id to fetch, or an array of object ids.
+     */
+    function getData($cid)
+    {
+        return isset($this->_dataCache[$cid]) ?
+            $this->_dataCache[$cid] :
+            array();
+    }
+
+    /**
+     * Retrieve data for an object.
+     *
+     * @param integer $cid  The object id to fetch.
+     */
+    function getAttributes($cid)
+    {
+        if (is_array($cid)) {
+            $data = array();
+            foreach ($cid as $id) {
+                if (isset($this->_attributeCache[$id])) {
+                    $data[$id] = $this->_attributeCache[$id];
+                }
+            }
+
+            return $data;
+        } else {
+            return isset($this->_attributeCache[$cid]) ?
+                $this->_attributeCache[$cid] :
+                array();
+        }
+    }
+
+    /**
+     * Returns the number of objects matching a set of attribute
+     * criteria.
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria   The array of criteria.
+     * @param string  $parent     The parent node to start searching from.
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent? Defaults to all levels.
+     * @param string  $restrict   Only return attributes with the same
+     *                            attribute_name or attribute_id.
+     */
+    function countByAttributes($criteria, $parent = DATATREE_ROOT, $allLevels = true, $restrict = 'name')
+    {
+        if (!count($criteria)) {
+            return 0;
+        }
+
+        return count($this->_attributeCache);
+    }
+
+    /**
+     * Returns a set of object ids based on a set of attribute criteria.
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param integer $from         The object to start to fetching
+     * @param integer $count        The number of objects to fetch
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     */
+    function getByAttributes($criteria, $parent = DATATREE_ROOT, $allLevels = true, $restrict = 'name', $from = 0, $count = 0,
+                             $sortby_name = null, $sortby_key = null, $direction = 0)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('no criteria');
+        }
+
+        $cids = array();
+        foreach (array_keys($this->_attributeCache) as $cid) {
+            $cids[$cid] = null;
+        }
+        return $cids;
+    }
+
+    /**
+     * Sorts IDs by attribute values. IDs without attributes will be
+     * added to the end of the sorted list.
+     *
+     * @param array $unordered_ids  Array of ids to sort.
+     * @param array $sortby_name    Attribute name to use for sorting.
+     * @param array $sortby_key     Attribute key to use for sorting.
+     * @param array $direction      Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return array  Sorted ids.
+     */
+    function sortByAttributes($unordered_ids, $sortby_name = null, $sortby_key = null, $direction = 0)
+    {
+        return $unordered_ids;
+    }
+
+    /**
+     * Returns a list of all of the available values of the given
+     * attribute name/key combination. Either attribute_name or
+     * attribute_key MUST be supplied, and both MAY be supplied.
+     *
+     * @param string $attribute_name  The name of the attribute.
+     * @param string $attribute_key   The key value of the attribute.
+     * @param string $parent          The parent node to start searching from.
+     * @param boolean $allLevels      Return all levels, or just the direct
+     *                                children of $parent?
+     *
+     * @return array  An array of all of the available values.
+     */
+    function getAttributeValues($attribute_name = null, $attribute_key = null, $parent = DATATREE_ROOT, $allLevels = true)
+    {
+        return array();
+    }
+
+    /**
+     * Update the data in an object. Does not change the object's
+     * parent or name, just serialized data.
+     *
+     * @param string $object  The object.
+     */
+    function updateData($object)
+    {
+        if (!is_a($object, 'DataTreeObject')) {
+            return true;
+        }
+
+        $cid = $this->getId($object->getName());
+        if (is_a($cid, 'PEAR_Error')) {
+            return $cid;
+        }
+
+        // We handle data differently if we can map it to
+        // attributes.
+        if (method_exists($object, '_toAttributes')) {
+            $this->_attributeCache[$cid] = $object->_toAttributes();
+        } else {
+            $this->_dataCache[$cid] = $object->getData();
+        }
+
+        return true;
+    }
+
+    /**
+     * Init the object.
+     *
+     * @return boolean  True.
+     */
+    function _init()
+    {
+       return true;
+    }
+
+}
diff --git a/framework/DataTree/DataTree/sql.php b/framework/DataTree/DataTree/sql.php
new file mode 100644 (file)
index 0000000..0e5c961
--- /dev/null
@@ -0,0 +1,1919 @@
+<?php
+/**
+ * The DataTree_sql:: class provides an SQL implementation of the Horde
+ * DataTree system.
+ *
+ * Required parameters:<pre>
+ *   'phptype'      The database type (ie. 'pgsql', 'mysql', etc.).
+ *   'charset'      The charset used by the database.</pre>
+ *
+ * Optional parameters:<pre>
+ *   'table'        The name of the data table in 'database'.
+ *                  DEFAULT: 'horde_datatree'</pre>
+ *
+ * Required by some database implementations:<pre>
+ *   'database'     The name of the database.
+ *   'username'     The username with which to connect to the database.
+ *   'password'     The password associated with 'username'.
+ *   'hostspec'     The hostname of the database server.
+ *   'protocol'     The communication protocol ('tcp', 'unix', etc.).
+ *   'options'      Additional options to pass to the database.
+ *   'port'         The port on which to connect to the database.
+ *   'tty'          The TTY on which to connect to the database.</pre>
+ *
+ * Optional values when using separate reading and writing servers, for example
+ * in replication settings:<pre>
+ *   'splitread'   Boolean, whether to implement the separation or not.
+ *   'read'        Array containing the parameters which are different for
+ *                 the read database connection, currently supported
+ *                 only 'hostspec' and 'port' parameters.</pre>
+ *
+ * The table structure for the DataTree system is in
+ * scripts/sql/horde_datatree.sql.
+ *
+ * $Horde: framework/DataTree/DataTree/sql.php,v 1.251 2009/07/09 08:17:54 slusarz Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If
+ * you did not receive this file, see
+ * http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stephane Huther <shuther1@free.fr>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 2.1
+ * @package Horde_DataTree
+ */
+class DataTree_sql extends DataTree {
+
+    /**
+     * Handle for the current database connection, used for reading.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $_db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * The number of copies of the horde_datatree_attributes table
+     * that we need to join on in the current query.
+     *
+     * @var integer
+     */
+    var $_tableCount = 1;
+
+    /**
+     * Returns a list of all groups (root nodes) of the data tree.
+     *
+     * @return array  The the group IDs
+     */
+    function getGroups()
+    {
+        $query = 'SELECT DISTINCT group_uid FROM ' .  $this->_params['table'];
+
+        Horde::logMessage('SQL Query by DataTree_sql::getGroups(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->getCol($query);
+    }
+
+    /**
+     * Loads (a subset of) the datatree into the $_data array.
+     *
+     * @access private
+     *
+     * @param string  $root         Which portion of the tree to load.
+     *                              Defaults to all of it.
+     * @param boolean $loadTree     Load a tree starting at $root, or just the
+     *                              requested level and direct parents?
+     *                              Defaults to single level.
+     * @param boolean $reload       Re-load already loaded values?
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                              0 - ascending
+     *                              1 - descending
+     *
+     * @return mixed  True on success or a PEAR_Error on failure.
+     */
+    function _load($root = DATATREE_ROOT, $loadTree = false, $reload = false,
+                   $sortby_name = null, $sortby_key = null, $direction = 0)
+    {
+        /* Do NOT use DataTree::exists() here; that would cause an infinite
+         * loop. */
+        if (!$reload &&
+            (in_array($root, $this->_nameMap) ||
+             (count($this->_data) && $root == DATATREE_ROOT)) ||
+            (!is_null($this->_sortHash) &&
+             isset($this->_data[$root]['sorter'][$this->_sortHash]))) {
+            return true;
+        }
+
+        $query = $this->_buildLoadQuery($root,
+                                        $loadTree,
+                                        DATATREE_BUILD_SELECT,
+                                        $sortby_name,
+                                        $sortby_key,
+                                        $direction);
+        if (empty($query)) {
+            return true;
+        }
+
+        Horde::logMessage('SQL Query by DataTree_sql::_load(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $data = $this->_db->getAll($query);
+        if (is_a($data, 'PEAR_Error')) {
+            return $data;
+        }
+        return $this->set($data, $this->_params['charset']);
+    }
+
+    /**
+     * Counts (a subset of) the datatree which would be loaded into the $_data
+     * array if _load() is called with the same value of $root.
+     *
+     * @access private
+     *
+     * @param string $root  Which portion of the tree to load. Defaults to all
+     *                      of it.
+     *
+     * @return integer  Number of objects
+     */
+    function _count($root = DATATREE_ROOT)
+    {
+        $query = $this->_buildLoadQuery($root, true, DATATREE_BUILD_COUNT);
+        if (empty($query)) {
+            return 0;
+        }
+        Horde::logMessage('SQL Query by DataTree_sql::_count(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return (int)$this->_db->getOne($query);
+    }
+
+    /**
+     * Loads (a subset of) the datatree into the $_data array.
+     *
+     * @access private
+     *
+     * @param string  $root         Which portion of the tree to load.
+     *                              Defaults to all of it.
+     * @param boolean $loadTree     Load a tree starting at $root, or just the
+     *                              requested level and direct parents?
+     *                              Defaults to single level.
+     * @param integer $operation    Type of query to build
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                              0 - ascending
+     *                              1 - descending
+     *
+     * @return mixed  True on success or a PEAR_Error on failure.
+     */
+    function _buildLoadQuery($root = DATATREE_ROOT, $loadTree = false,
+                             $operation = DATATREE_BUILD_SELECT,
+                             $sortby_name = null, $sortby_key = null,
+                             $direction = 0)
+    {
+        $sorted = false;
+        $where = sprintf('c.group_uid = %s ', $this->_db->quote($this->_params['group']));
+
+        if (!empty($root) && $root != DATATREE_ROOT) {
+            $parent_where = $this->_buildParentIds($root, $loadTree, 'c.');
+            if (empty($parent_where)) {
+                return '';
+            } elseif (!is_a($parent_where, 'PEAR_Error')) {
+                $where = sprintf('%s AND (%s)', $where, $parent_where);
+            }
+        }
+        if (!is_null($sortby_name)) {
+            $where = sprintf('%s AND a.attribute_name = %s ', $where, $this->_db->quote($sortby_name));
+            $sorted = true;
+        }
+        if (!is_null($sortby_key)) {
+            $where = sprintf('%s AND a.attribute_key = %s ', $where, $this->_db->quote($sortby_key));
+            $sorted = true;
+        }
+
+        switch ($operation) {
+        case DATATREE_BUILD_COUNT:
+            $what = 'COUNT(*)';
+            break;
+
+        default:
+            $what = 'c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order';
+            break;
+        }
+
+        if ($sorted) {
+            $query = sprintf('SELECT %s FROM %s c LEFT JOIN %s a ON (c.datatree_id = a.datatree_id OR c.datatree_name=%s) '.
+                             'WHERE %s GROUP BY c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order ORDER BY a.attribute_value %s',
+                             $what,
+                             $this->_params['table'],
+                             $this->_params['table_attributes'],
+                             $this->_db->quote($root),
+                             $where,
+                             ($direction == 1) ? 'DESC' : 'ASC');
+        } else {
+            $query = sprintf('SELECT %s FROM %s c WHERE %s',
+                             $what,
+                             $this->_params['table'],
+                             $where);
+        }
+
+        return $query;
+    }
+
+    /**
+     * Builds parent ID string for selecting trees.
+     *
+     * @access private
+     *
+     * @param string  $root      Which portion of the tree to load.
+     * @param boolean $loadTree  Load a tree starting at $root, or just the
+     *                           requested level and direct parents?
+     *                           Defaults to single level.
+     * @param string  $join_name Table join name
+     *
+     * @return string  Id list.
+     */
+    function _buildParentIds($root, $loadTree = false, $join_name = '')
+    {
+        if (strpos($root, ':') !== false) {
+            $parts = explode(':', $root);
+            $root = array_pop($parts);
+        }
+        $root = (string)$root;
+
+        $query = 'SELECT datatree_id, datatree_parents' .
+            ' FROM ' . $this->_params['table'] .
+            ' WHERE datatree_name = ? AND group_uid = ?' .
+            ' ORDER BY datatree_id';
+        $values = array($root,
+                        $this->_params['group']);
+
+        Horde::logMessage('SQL Query by DataTree_sql::_buildParentIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $root = $this->_db->getAssoc($query, false, $values);
+        if (is_a($root, 'PEAR_Error') || !count($root)) {
+            return '';
+        }
+
+        $where = '';
+        $first_time = true;
+        foreach ($root as $object_id => $object_parents) {
+            $pstring = $object_parents . ':' . $object_id . '%';
+            $pquery = '';
+            if (!empty($object_parents)) {
+                $ids = substr($object_parents, 1);
+                $pquery = ' OR ' . $join_name . 'datatree_id IN (' . str_replace(':', ', ', $ids) . ')';
+            }
+            if ($loadTree) {
+                $pquery .= ' OR ' . $join_name . 'datatree_parents = ' . $this->_db->quote(substr($pstring, 0, -1));
+            }
+
+            if (!$first_time) {
+                $where .= ' OR ';
+            }
+            $where .= sprintf($join_name . 'datatree_parents LIKE %s OR ' . $join_name . 'datatree_id = %s%s',
+                              $this->_db->quote($pstring),
+                              $object_id,
+                              $pquery);
+
+            $first_time = false;
+        }
+
+        return $where;
+    }
+
+    /**
+     * Loads a set of objects identified by their unique IDs, and their
+     * parents, into the $_data array.
+     *
+     * @access private
+     *
+     * @param mixed $cids  The unique ID of the object to load, or an array of
+     *                     object ids.
+     *
+     * @return mixed  True on success or a PEAR_Error on failure.
+     */
+    function _loadById($cids)
+    {
+        /* Make sure we have an array. */
+        if (!is_array($cids)) {
+            $cids = array((int)$cids);
+        } else {
+            array_walk($cids, 'intval');
+        }
+
+        /* Bail out now if there's nothing to load. */
+        if (!count($cids)) {
+            return true;
+        }
+
+        /* Don't load any that are already loaded. Also, make sure that
+         * everything in the $ids array that we are building is an integer. */
+        $ids = array();
+        foreach ($cids as $cid) {
+            /* Do NOT use DataTree::exists() here; that would cause an
+             * infinite loop. */
+            if (!isset($this->_data[$cid])) {
+                $ids[] = (int)$cid;
+            }
+        }
+
+        /* If there are none left to load, return. */
+        if (!count($ids)) {
+            return true;
+        }
+
+        $in = array_search(DATATREE_ROOT, $ids) === false ? sprintf('datatree_id IN (%s) AND ', implode(', ', $ids)) : '';
+        $query = sprintf('SELECT datatree_id, datatree_parents FROM %s' .
+                         ' WHERE %sgroup_uid = %s' .
+                         ' ORDER BY datatree_id',
+                         $this->_params['table'],
+                         $in,
+                         $this->_db->quote($this->_params['group']));
+        Horde::logMessage('SQL Query by DataTree_sql::_loadById(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $parents = $this->_db->getAssoc($query);
+        if (is_a($parents, 'PEAR_Error')) {
+            return $parents;
+        }
+        if (empty($parents)) {
+            return PEAR::raiseError(_("Object not found."), null, null, null, 'DataTree ids ' . implode(', ', $ids) . ' not found.');
+        }
+
+        $ids = array();
+        foreach ($parents as $cid => $parent) {
+            $ids[(int)$cid] = (int)$cid;
+
+            $pids = explode(':', substr($parent, 1));
+            foreach ($pids as $pid) {
+                $pid = (int)$pid;
+                if (!isset($this->_data[$pid])) {
+                    $ids[$pid] = $pid;
+                }
+            }
+        }
+
+        /* If $ids is empty, we have nothing to load. */
+        if (!count($ids)) {
+            return true;
+        }
+
+        $query = 'SELECT datatree_id, datatree_name, datatree_parents, datatree_order' .
+                 ' FROM ' . $this->_params['table'] .
+                 ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')' .
+                 ' AND group_uid = ? ORDER BY datatree_id';
+        $values = array_merge($ids, array($this->_params['group']));
+
+        Horde::logMessage('SQL Query by DataTree_sql::_loadById(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $data = $this->_db->getAll($query, $values);
+        if (is_a($data, 'PEAR_Error')) {
+            return $data;
+        }
+
+        return $this->set($data, $this->_params['charset']);
+    }
+
+    /**
+     * Check for existance of an object in a backend-specific manner.
+     *
+     * @param string $object_name Object name to check for.
+     *
+     * @return boolean True if the object exists, false otherwise.
+     */
+    function _exists($object_name)
+    {
+        $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
+            ' WHERE group_uid = ? AND datatree_name = ? AND datatree_parents = ?';
+
+        $object_names = explode(':', $object_name);
+        $object_parents = '';
+        foreach ($object_names as $name) {
+            $values = array($this->_params['group'], $name, $object_parents);
+            Horde::logMessage('SQL Query by DataTree_sql::_exists(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $result = $this->_db->getOne($query, $values);
+            if (is_a($result, 'PEAR_Error') || !$result) {
+                return false;
+            }
+
+            $object_parents .= ':' . $result;
+        }
+
+        return true;
+    }
+
+    /**
+     * Look up a datatree id by name.
+     *
+     * @param string $name
+     *
+     * @return integer DataTree id
+     */
+    function _getId($name)
+    {
+        $query = 'SELECT datatree_id FROM ' . $this->_params['table']
+            . ' WHERE group_uid = ? AND datatree_name = ?'
+            . ' AND datatree_parents = ?';
+
+        $ids = array();
+        $parts = explode(':', $name);
+        foreach ($parts as $part) {
+            $result = $this->_db->getOne($query, array($this->_params['group'], $part, count($ids) ? ':' . implode(':', $ids) : ''));
+            if (is_a($result, 'PEAR_Error') || !$result) {
+                return null;
+            } else {
+                $ids[] = $result;
+            }
+        }
+
+        return (int)array_pop($ids);
+    }
+
+    /**
+     * Look up a datatree name by id.
+     *
+     * @param integer $id
+     *
+     * @return string DataTree name
+     */
+    function _getName($id)
+    {
+        $query = 'SELECT datatree_name FROM ' . $this->_params['table'] .
+            ' WHERE group_uid = ? AND datatree_id = ?';
+        $values = array($this->_params['group'], (int)$id);
+        Horde::logMessage('SQL Query by DataTree_sql::_getName(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $name = $this->_db->getOne($query, $values);
+        if (is_a($name, 'PEAR_Error')) {
+            return null;
+        } else {
+            $name = Horde_String::convertCharset($name, $this->_params['charset'],
+                                           Horde_Nls::getCharset());
+            // Get the parent names, if any.
+            $parent = $this->getParentById($id);
+            if ($parent && !is_a($parent, 'PEAR_Error') &&
+                $parent != DATATREE_ROOT) {
+                return $this->getName($parent) . ':' . $name;
+            } else {
+                return $name;
+            }
+        }
+    }
+
+    /**
+     * Returns a tree sorted by the specified attribute name and/or key.
+     *
+     * @since Horde 3.1
+     *
+     * @param string $root         Which portion of the tree to sort.
+     *                             Defaults to all of it.
+     * @param boolean $loadTree    Sort the tree starting at $root, or just the
+     *                             requested level and direct parents?
+     *                             Defaults to single level.
+     * @param string $sortby_name  Attribute name to use for sorting.
+     * @param string $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction   Sort direction:
+     *                             0 - ascending
+     *                             1 - descending
+     *
+     * @return array TODO
+     */
+    function getSortedTree($root, $loadTree = false, $sortby_name = null,
+                           $sortby_key = null, $direction = 0)
+    {
+        $query = $this->_buildLoadQuery($root,
+                                        $loadTree,
+                                        DATATREE_BUILD_SELECT,
+                                        $sortby_name,
+                                        $sortby_key,
+                                        $direction);
+
+        if (empty($query)) {
+            return array();
+        }
+        return $this->_db->getAll($query);
+    }
+
+    /**
+     * Adds an object.
+     *
+     * @param mixed $object        The object to add (string or
+     *                             DataTreeObject).
+     * @param boolean $id_as_name  Whether the object ID is to be used as
+     *                             object name.  Used in situations where
+     *                             there is no available unique input for
+     *                             object name.
+     */
+    function add($object, $id_as_name = false)
+    {
+        $attributes = false;
+        if (is_a($object, 'DataTreeObject')) {
+            $fullname = $object->getName();
+            $order = $object->order;
+
+            /* We handle data differently if we can map it to the
+             * horde_datatree_attributes table. */
+            if (method_exists($object, '_toAttributes')) {
+                $data = '';
+                $ser = null;
+
+                /* Set a flag for later so that we know to insert the
+                 * attribute rows. */
+                $attributes = true;
+            } else {
+                require_once 'Horde/Serialize.php';
+                $ser = Horde_Serialize::UTF7_BASIC;
+                $data = Horde_Serialize::serialize($object->getData(), $ser, Horde_Nls::getCharset());
+            }
+        } else {
+            $fullname = $object;
+            $order = null;
+            $data = '';
+            $ser = null;
+        }
+
+        /* Get the next unique ID. */
+        $id = $this->_write_db->nextId($this->_params['table']);
+        if (is_a($id, 'PEAR_Error')) {
+            Horde::logMessage($id, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $id;
+        }
+
+        if (strpos($fullname, ':') !== false) {
+            $parts = explode(':', $fullname);
+            $parents = '';
+            $pstring = '';
+            if ($id_as_name) {
+                /* Requested use of ID as name, so discard current name. */
+                array_pop($parts);
+                /* Set name to ID. */
+                $name = $id;
+                /* Modify fullname to reflect new name. */
+                $fullname = implode(':', $parts) . ':' . $id;
+                if (is_a($object, 'DataTreeObject')) {
+                    $object->setName($fullname);
+                } else {
+                    $object = $fullname;
+                }
+            } else {
+                $name = array_pop($parts);
+            }
+            foreach ($parts as $par) {
+                $pstring .= (empty($pstring) ? '' : ':') . $par;
+                $pid = $this->getId($pstring);
+                if (is_a($pid, 'PEAR_Error')) {
+                    /* Auto-create parents. */
+                    $pid = $this->add($pstring);
+                    if (is_a($pid, 'PEAR_Error')) {
+                        return $pid;
+                    }
+                }
+                $parents .= ':' . $pid;
+            }
+        } else {
+            if ($id_as_name) {
+                /* Requested use of ID as name, set fullname and name to ID. */
+                $fullname = $id;
+                $name = $id;
+                if (is_a($object, 'DataTreeObject')) {
+                    $object->setName($fullname);
+                } else {
+                    $object = $fullname;
+                }
+            } else {
+                $name = $fullname;
+            }
+            $parents = '';
+            $pid = DATATREE_ROOT;
+        }
+
+        if (parent::exists($fullname)) {
+            return PEAR::raiseError(sprintf(_("\"%s\" already exists"), $fullname));
+        }
+
+        $query = 'INSERT INTO ' . $this->_params['table'] .
+                 ' (datatree_id, group_uid, datatree_name, datatree_order,' .
+                 ' datatree_data, user_uid, datatree_serialized,' .
+                 ' datatree_parents)' .
+                 ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
+        $values = array((int)$id,
+                        $this->_params['group'],
+                        Horde_String::convertCharset($name, Horde_Nls::getCharset(), $this->_params['charset']),
+                        is_null($order) ? NULL : (int)$order,
+                        $data,
+                        (string)Horde_Auth::getAuth(),
+                        (int)$ser,
+                        $parents);
+
+        Horde::logMessage('SQL Query by DataTree_sql::add(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        $reorder = $this->reorder($parents, $order, $id);
+        if (is_a($reorder, 'PEAR_Error')) {
+            Horde::logMessage($reorder, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $reorder;
+        }
+
+        $result = parent::_add($fullname, $id, $pid, $order);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* If we succesfully inserted the object and it supports
+         * being mapped to the attributes table, do that now: */
+        if (!empty($attributes)) {
+            $result = $this->updateData($object);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return $id;
+    }
+
+    /**
+     * Changes the order of the children of an object.
+     *
+     * @param string $parent  The full id path of the parent object.
+     * @param mixed $order    If an array it specifies the new positions for
+     *                        all child objects.
+     *                        If an integer and $cid is specified, the position
+     *                        where the child specified by $cid is inserted. If
+     *                        $cid is not specified, the position gets deleted,
+     *                        causing the following positions to shift up.
+     * @param integer $cid    See $order.
+     */
+    function reorder($parent, $order = null, $cid = null)
+    {
+        if (!$parent || is_a($parent, 'PEAR_Error')) {
+            // Abort immediately if the parent string is empty; we
+            // cannot safely reorder all top-level elements.
+            return;
+        }
+
+        $pquery = '';
+        if (!is_array($order) && !is_null($order)) {
+            /* Single update (add/del). */
+            if (is_null($cid)) {
+                /* No object id given so shuffle down. */
+                $direction = '-';
+            } else {
+                /* We have an object id so shuffle up. */
+                $direction = '+';
+
+                /* Leaving the newly inserted object alone. */
+                $pquery = sprintf(' AND datatree_id != %s', (int)$cid);
+            }
+            $query = sprintf('UPDATE %s SET datatree_order = datatree_order %s 1 WHERE group_uid = %s AND datatree_parents = %s AND datatree_order >= %s',
+                             $this->_params['table'],
+                             $direction,
+                             $this->_write_db->quote($this->_params['group']),
+                             $this->_write_db->quote($parent),
+                             is_null($order) ? 'NULL' : (int)$order) . $pquery;
+
+            Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $result = $this->_write_db->query($query);
+        } elseif (is_array($order)) {
+            /* Multi update. */
+            $query = 'SELECT COUNT(datatree_id)' .
+                     ' FROM ' . $this->_params['table'] .
+                     ' WHERE group_uid = ? AND datatree_parents = ?' .
+                     ' GROUP BY datatree_parents';
+            $values = array($this->_params['group'],
+                            $parent);
+
+            Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $result = $this->_db->getOne($query, $values);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            } elseif (count($order) != $result) {
+                return PEAR::raiseError(_("Cannot reorder, number of entries supplied for reorder does not match number stored."));
+            }
+
+            $o_key = 0;
+            foreach ($order as $o_cid) {
+                $query = 'UPDATE ' . $this->_params['table'] .
+                         ' SET datatree_order = ? WHERE datatree_id = ?';
+                $values = array($o_key, is_null($o_cid) ? NULL : (int)$o_cid);
+
+                Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $result = $this->_write_db->query($query, $values);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+
+                $o_key++;
+            }
+
+            $pid = $this->getId($parent);
+
+            /* Re-order our cache. */
+            return $this->_reorder($pid, $order);
+        }
+    }
+
+    /**
+     * Explicitly set the order for a datatree object.
+     *
+     * @param integer $id     The datatree object id to change.
+     * @param integer $order  The new order.
+     */
+    function setOrder($id, $order)
+    {
+        $query = 'UPDATE ' . $this->_params['table'] .
+                 ' SET datatree_order = ? WHERE datatree_id = ?';
+        $values = array(is_null($order) ? NULL : (int)$order,
+                        (int)$id);
+
+        Horde::logMessage('SQL Query by DataTree_sql::setOrder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $this->_write_db->query($query, $values);
+    }
+
+    /**
+     * Removes an object.
+     *
+     * @param mixed   $object  The object to remove.
+     * @param boolean $force   Force removal of every child object?
+     */
+    function remove($object, $force = false)
+    {
+        $id = $this->getId($object);
+        if (is_a($id, 'PEAR_Error')) {
+            return $id;
+        }
+        $order = $this->getOrder($object);
+
+        $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
+                 ' WHERE group_uid = ? AND datatree_parents LIKE ?' .
+                 ' ORDER BY datatree_id';
+        $values = array($this->_params['group'],
+                        '%:' . (int)$id . '');
+
+        Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $children = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC);
+
+        if (count($children)) {
+            if ($force) {
+                foreach ($children as $child) {
+                    $cat = $this->getName($child['datatree_id']);
+                    $result = $this->remove($cat, true);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                }
+            } else {
+                return PEAR::raiseError(sprintf(_("Cannot remove, %d children exist."), count($children)));
+            }
+        }
+
+        /* Remove attributes for this object. */
+        $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
+                 ' WHERE datatree_id = ?';
+        $values = array((int)$id);
+
+        Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'DELETE FROM ' . $this->_params['table'] .
+                 ' WHERE datatree_id = ?';
+        $values = array((int)$id);
+
+        Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $parents = $this->getParentIdString($object);
+        if (is_a($parents, 'PEAR_Error')) {
+            return $parents;
+        }
+
+        $reorder = $this->reorder($parents, $order);
+        if (is_a($reorder, 'PEAR_Error')) {
+            return $reorder;
+        }
+
+        return is_a(parent::remove($object), 'PEAR_Error') ? $id : true;
+    }
+
+    /**
+     * Removes one or more objects by id.
+     *
+     * This function does *not* do the validation, reordering, etc. that
+     * remove() does. If you need to check for children, re-do ordering, etc.,
+     * then you must remove() objects one-by-one. This is for code that knows
+     * it's dealing with single (non-parented) objects and needs to delete a
+     * batch of them quickly.
+     *
+     * @param array $ids  The objects to remove.
+     */
+    function removeByIds($ids)
+    {
+        /* Sanitize input. */
+        if (!is_array($ids)) {
+            $ids = array((int)$ids);
+        } else {
+            array_walk($ids, 'intval');
+        }
+
+        /* Removing zero objects always succeeds. */
+        if (!$ids) {
+            return true;
+        }
+
+        /* Remove attributes for $ids. */
+        $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
+                 ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')';
+        $values = $ids;
+
+        Horde::logMessage('SQL Query by DataTree_sql::removeByIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'DELETE FROM ' . $this->_params['table'] .
+                 ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')';
+        $values = $ids;
+
+        Horde::logMessage('SQL Query by DataTree_sql::removeByIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $this->_write_db->query($query, $values);
+    }
+
+    /**
+     * Removes one or more objects by name.
+     *
+     * This function does *not* do the validation, reordering, etc. that
+     * remove() does. If you need to check for children, re-do ordering, etc.,
+     * then you must remove() objects one-by-one. This is for code that knows
+     * it's dealing with single (non-parented) objects and needs to delete a
+     * batch of them quickly.
+     *
+     * @param array $names  The objects to remove.
+     */
+    function removeByNames($names)
+    {
+        if (!is_array($names)) {
+            $names = array($names);
+        }
+
+        /* Removing zero objects always succeeds. */
+        if (!$names) {
+            return true;
+        }
+
+        $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
+                 ' WHERE datatree_name IN (?' . str_repeat(', ?', count($names) - 1) . ')';
+        $values = $names;
+
+        Horde::logMessage('SQL Query by DataTree_sql::removeByNames(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $ids = $this->_db->getCol($query, 0, $values);
+        if (is_a($ids, 'PEAR_Error')) {
+            return $ids;
+        }
+
+        return $this->removeByIds($ids);
+    }
+
+    /**
+     * Move an object to a new parent.
+     *
+     * @param mixed  $object     The object to move.
+     * @param string $newparent  The new parent object. Defaults to the root.
+     */
+    function move($object, $newparent = null)
+    {
+        $old_parent_path = $this->getParentIdString($object);
+        $result = parent::move($object, $newparent);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $id = $this->getId($object);
+        $new_parent_path = $this->getParentIdString($object);
+
+        /* Fetch the object being moved and all of its children, since
+         * we also need to update their parent paths to avoid creating
+         * orphans. */
+        $query = 'SELECT datatree_id, datatree_parents' .
+                 ' FROM ' . $this->_params['table'] .
+                 ' WHERE datatree_parents = ? OR datatree_parents LIKE ?' .
+                 ' OR datatree_id = ?';
+        $values = array($old_parent_path . ':' . $id,
+                        $old_parent_path . ':' . $id . ':%',
+                        (int)$id);
+
+        Horde::logMessage('SQL Query by DataTree_sql::move(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $rowset = $this->_db->query($query, $values);
+        if (is_a($rowset, 'PEAR_Error')) {
+            return $rowset;
+        }
+
+        /* Update each object, replacing the old parent path with the
+         * new one. */
+        while ($row = $rowset->fetchRow(DB_FETCHMODE_ASSOC)) {
+            if (is_a($row, 'PEAR_Error')) {
+                return $row;
+            }
+
+            $oquery = '';
+            if ($row['datatree_id'] == $id) {
+                $oquery = ', datatree_order = 0 ';
+            }
+
+            /* Do str_replace() only if this is not a first level
+             * object. */
+            if (!empty($row['datatree_parents'])) {
+                $ppath = str_replace($old_parent_path, $new_parent_path, $row['datatree_parents']);
+            } else {
+                $ppath = $new_parent_path;
+            }
+            $query = sprintf('UPDATE %s SET datatree_parents = %s' . $oquery . ' WHERE datatree_id = %s',
+                             $this->_params['table'],
+                             $this->_write_db->quote($ppath),
+                             (int)$row['datatree_id']);
+
+            Horde::logMessage('SQL Query by DataTree_sql::move(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $result = $this->_write_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $order = $this->getOrder($object);
+
+        /* Shuffle down the old order positions. */
+        $reorder = $this->reorder($old_parent_path, $order);
+
+        /* Shuffle up the new order positions. */
+        $reorder = $this->reorder($new_parent_path, 0, $id);
+
+        return true;
+    }
+
+    /**
+     * Change an object's name.
+     *
+     * @param mixed  $old_object       The old object.
+     * @param string $new_object_name  The new object name.
+     */
+    function rename($old_object, $new_object_name)
+    {
+        /* Do the cache renaming first */
+        $result = parent::rename($old_object, $new_object_name);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Get the object id and set up the sql query. */
+        $id = $this->getId($old_object);
+        $query = 'UPDATE ' . $this->_params['table'] .
+                 ' SET datatree_name = ? WHERE datatree_id = ?';
+        $values = array(Horde_String::convertCharset($new_object_name, Horde_Nls::getCharset(), $this->_params['charset']),
+                        (int)$id);
+
+        Horde::logMessage('SQL Query by DataTree_sql::rename(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_write_db->query($query, $values);
+
+        return is_a($result, 'PEAR_Error') ? $result : true;
+    }
+
+    /**
+     * Retrieves data for an object from the datatree_data field.
+     *
+     * @param integer $cid  The object id to fetch, or an array of object ids.
+     */
+    function getData($cid)
+    {
+        require_once 'Horde/Serialize.php';
+
+        if (is_array($cid)) {
+            if (!count($cid)) {
+                return array();
+            }
+
+            $query = sprintf('SELECT datatree_id, datatree_data, datatree_serialized FROM %s WHERE datatree_id IN (%s)',
+                             $this->_params['table'],
+                             implode(', ', $cid));
+
+            Horde::logMessage('SQL Query by DataTree_sql::getData(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $result = $this->_db->getAssoc($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            }
+
+            $data = array();
+            foreach ($result as $id => $row) {
+                $data[$id] = Horde_Serialize::unserialize($row[0], $row[1],
+                                                          Horde_Nls::getCharset());
+                /* Convert old data to the new format. */
+                if ($row[1] == Horde_Serialize::BASIC) {
+                    $data[$id] = Horde_String::convertCharset($data[$id],
+                                                        Horde_Nls::getCharset(true));
+                }
+
+                $data[$id] = (is_null($data[$id]) || !is_array($data[$id]))
+                    ? array()
+                    : $data[$id];
+            }
+
+            return $data;
+        } else {
+            $query = 'SELECT datatree_data, datatree_serialized' .
+                     ' FROM ' . $this->_params['table'] .
+                     ' WHERE datatree_id = ?';
+            $values = array((int)$cid);
+
+            Horde::logMessage('SQL Query by DataTree_sql::getData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $row = $this->_db->getRow($query, $values, DB_FETCHMODE_ASSOC);
+
+            $data = Horde_Serialize::unserialize($row['datatree_data'],
+                                                 $row['datatree_serialized'],
+                                                 Horde_Nls::getCharset());
+            /* Convert old data to the new format. */
+            if ($row['datatree_serialized'] == Horde_Serialize::BASIC) {
+                $data = Horde_String::convertCharset($data, Horde_Nls::getCharset(true));
+            }
+            return (is_null($data) || !is_array($data)) ? array() : $data;
+        }
+    }
+
+    /**
+     * Retrieves data for an object from the horde_datatree_attributes table.
+     *
+     * @param integer|array $cid  The object id to fetch, or an array of
+     *                            object ids.
+     * @param array $keys         The attributes keys to fetch.
+     *
+     * @return array  A hash of attributes, or a multi-level hash of object
+     *                ids => their attributes.
+     */
+    function getAttributes($cid, $keys = false)
+    {
+        if (empty($cid)) {
+            return array();
+        }
+
+        if ($keys) {
+            $filter = sprintf(' AND attribute_key IN (\'%s\')',
+                              implode("', '", $keys));
+        } else {
+            $filter = '';
+        }
+
+        if (is_array($cid)) {
+            $query = sprintf('SELECT datatree_id, attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id IN (%s)%s',
+                             $this->_params['table_attributes'],
+                             implode(', ', $cid),
+                             $filter);
+
+            Horde::logMessage('SQL Query by DataTree_sql::getAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC);
+            if (is_a($rows, 'PEAR_Error')) {
+                return $rows;
+            }
+
+            $data = array();
+            foreach ($rows as $row) {
+                if (empty($data[$row['datatree_id']])) {
+                    $data[$row['datatree_id']] = array();
+                }
+                $data[$row['datatree_id']][] = array('name' => $row['name'],
+                                                     'key' => $row['key'],
+                                                     'value' => Horde_String::convertCharset($row['value'], $this->_params['charset'], Horde_Nls::getCharset()));
+            }
+            return $data;
+        } else {
+            $query = sprintf('SELECT attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id = %s%s',
+                             $this->_params['table_attributes'],
+                             (int)$cid,
+                             $filter);
+
+            Horde::logMessage('SQL Query by DataTree_sql::getAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC);
+            for ($i = 0; $i < count($rows); $i++) {
+                $rows[$i]['value'] = Horde_String::convertCharset($rows[$i]['value'],
+                                                            $this->_params['charset'],
+                                                            Horde_Nls::getCharset());
+            }
+            return $rows;
+        }
+    }
+
+    /**
+     * Returns the number of objects matching a set of attribute criteria.
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria   The array of criteria.
+     * @param string  $parent     The parent node to start searching from.
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent? Defaults to all levels.
+     * @param string  $restrict   Only return attributes with the same
+     *                            attribute_name or attribute_id.
+     */
+    function countByAttributes($criteria, $parent = DATATREE_ROOT,
+                               $allLevels = true, $restrict = 'name')
+    {
+        if (!count($criteria)) {
+            return 0;
+        }
+
+        $aq = $this->buildAttributeQuery($criteria,
+                                         $parent,
+                                         $allLevels,
+                                         $restrict,
+                                         DATATREE_BUILD_COUNT);
+        if (is_a($aq, 'PEAR_Error')) {
+            return $aq;
+        }
+        list($query, $values) = $aq;
+
+        Horde::logMessage('SQL Query by DataTree_sql::countByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+        $row = $result->fetchRow();
+        if (is_a($row, 'PEAR_Error')) {
+            Horde::logMessage($row, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $row;
+        }
+
+        return $row[0];
+    }
+
+    /**
+     * Returns a set of object ids based on a set of attribute criteria.
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param integer $from         The object to start to fetching
+     * @param integer $count        The number of objects to fetch
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     */
+    function getByAttributes($criteria, $parent = DATATREE_ROOT,
+                             $allLevels = true, $restrict = 'name', $from = 0,
+                             $count = 0, $sortby_name = null,
+                             $sortby_key = null, $direction = 0)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('no criteria');
+        }
+
+        // If there are top-level OR criteria, process one at a time
+        // and return any results as soon as they're found...but only if
+        // there is no LIMIT requested.
+        if ($count == 0 && $from == 0) {
+            foreach ($criteria as $key => $vals) {
+                if ($key == 'OR') {
+                    $rows = array();
+                    $num_or_statements = count($criteria[$key]);
+                    for ($i = 0; $i < $num_or_statements; $i++) {
+                        $criteria_or = $criteria['OR'][$i];
+                        list($query, $values) = $this->buildAttributeQuery(
+                                                    $criteria_or,
+                                                    $parent,
+                                                    $allLevels,
+                                                    $restrict,
+                                                    DATATREE_BUILD_SELECT,
+                                                    $sortby_name,
+                                                    $sortby_key,
+                                                    $direction);
+                        if ($count) {
+                            $query = $this->_db->modifyLimitQuery($query, $from, $count);
+                        }
+
+                        Horde::logMessage('SQL Query by DataTree_sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                        $result = $this->_db->query($query, $values);
+                        if (is_a($result, 'PEAR_Error')) {
+                            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                            return $result;
+                        }
+                        while ($row = $result->fetchRow()) {
+                            $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset']);
+                        }
+                    }
+
+                    return $rows;
+                }
+            }
+        }
+        // Process AND or other complex queries.
+        $aq = $this->buildAttributeQuery($criteria,
+                                         $parent,
+                                         $allLevels,
+                                         $restrict,
+                                         DATATREE_BUILD_SELECT,
+                                         $sortby_name,
+                                         $sortby_key,
+                                         $direction);
+        if (is_a($aq, 'PEAR_Error')) {
+            return $aq;
+        }
+
+        list($query, $values) = $aq;
+
+        if ($count) {
+            $query = $this->_db->modifyLimitQuery($query, $from, $count);
+        }
+
+        Horde::logMessage('SQL Query by DataTree_sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        $rows = array();
+        while ($row = $result->fetchRow()) {
+            $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset']);
+        }
+
+        return $rows;
+    }
+
+    /**
+     * Sorts IDs by attribute values. IDs without attributes will be added to
+     * the end of the sorted list.
+     *
+     * @param array $unordered_ids  Array of ids to sort.
+     * @param array $sortby_name    Attribute name to use for sorting.
+     * @param array $sortby_key     Attribute key to use for sorting.
+     * @param array $direction      Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return array  Sorted ids.
+     */
+    function sortByAttributes($unordered_ids, $sortby_name = null,
+                              $sortby_key = null, $direction = 0)
+    {
+        /* Select ids ordered by attribute value. */
+        $where = '';
+        if (!is_null($sortby_name)) {
+            $where = sprintf(' AND attribute_name = %s ',
+                             $this->_db->quote($sortby_name));
+        }
+        if (!is_null($sortby_key)) {
+            $where = sprintf('%s AND attribute_key = %s ',
+                             $where,
+                             $this->_db->quote($sortby_key));
+        }
+
+        $query = sprintf('SELECT datatree_id FROM %s WHERE datatree_id IN (%s) %s ORDER BY attribute_value %s',
+                         $this->_params['table_attributes'],
+                         implode(',', $unordered_ids),
+                         $where,
+                         ($direction == 1) ? 'DESC' : 'ASC');
+
+        Horde::logMessage('SQL Query by DataTree_sql::sortByAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $ordered_ids = $this->_db->getCol($query);
+
+        /* Make sure that some ids didn't get lost because has no such
+         * attribute name/key. Append them to the end. */
+        if (count($ordered_ids) != count($unordered_ids)) {
+            $ordered_ids = array_keys(array_flip(array_merge($ordered_ids, $unordered_ids)));
+        }
+
+        return $ordered_ids;
+    }
+
+    /**
+     * Returns the number of all of the available values matching the
+     * given criteria. Either attribute_name or attribute_key MUST be
+     * supplied, and both MAY be supplied.
+     *
+     * @since Horde 3.2
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param string  $attribute_name  The name of the attribute.
+     * @param string  $attribute_key   The key value of the attribute.
+     */
+    function countValuesByAttributes($criteria, $parent = DATATREE_ROOT,
+                                     $allLevels = true, $restrict = 'name',
+                                     $key = null, $name = null)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('no criteria');
+        }
+
+        $aq = $this->buildAttributeQuery($criteria,
+                                         $parent,
+                                         $allLevels,
+                                         $restrict,
+                                         DATATREE_BUILD_VALUES_COUNT);
+
+        $aq[0] .= ' AND a.datatree_id = c.datatree_id';
+
+        if ($key !== null) {
+            $aq[0] .=  ' AND a.attribute_key = ?';
+            $aq[1][] = $key;
+        }
+
+        if ($name !== null) {
+            $aq[0] .=  ' AND a.attribute_name = ?';
+            $aq[1][] = $name;
+        }
+
+        return $this->_db->getOne($aq[0], $aq[1]);
+    }
+
+    /**
+     * Returns a list of all of the available values of the given criteria
+     * Either attribute_name or attribute_key MUST be
+     * supplied, and both MAY be supplied.
+     *
+     * @since Horde 3.2
+     *
+     * @see buildAttributeQuery()
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param integer $from         The object to start to fetching
+     * @param integer $count        The number of objects to fetch
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     * @param string  $attribute_name  The name of the attribute.
+     * @param string  $attribute_key   The key value of the attribute.
+     */
+    function getValuesByAttributes($criteria, $parent = DATATREE_ROOT,
+                                   $allLevels = true, $restrict = 'name', $from = 0,
+                                   $count = 0, $sortby_name = null,
+                                   $sortby_key = null, $direction = 0,
+                                   $key = null, $name = null)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('no criteria');
+        }
+
+        $aq = $this->buildAttributeQuery($criteria,
+                                         $parent,
+                                         $allLevels,
+                                         $restrict,
+                                         DATATREE_BUILD_VALUES,
+                                         $sortby_name,
+                                         $sortby_key,
+                                         $direction);
+
+        $aq[0] .=  ' AND a.datatree_id = c.datatree_id';
+
+        if ($key !== null) {
+            $aq[0] .=  ' AND a.attribute_key = ?';
+            $aq[1][] = $key;
+        }
+
+        if ($name !== null) {
+            $aq[0] .=  ' AND a.attribute_name = ?';
+            $aq[1][] = $name;
+        }
+
+        if ($count) {
+            $aq[0] = $this->_db->modifyLimitQuery($aq[0], $from, $count);
+        }
+
+        return $this->_db->getCol($aq[0], 0, $aq[1]);
+    }
+
+    /**
+     * Returns a list of all of the available values of the given attribute
+     * name/key combination. Either attribute_name or attribute_key MUST be
+     * supplied, and both MAY be supplied.
+     *
+     * @param string  $attribute_name  The name of the attribute.
+     * @param string  $attribute_key   The key value of the attribute.
+     * @param string  $parent          The parent node to start searching from.
+     * @param boolean $allLevels       Return all levels, or just the direct
+     *                                 children of $parent? Defaults to all
+     *                                 levels.
+     *
+     * @return array  An array of all of the available values.
+     */
+    function getAttributeValues($attribute_name = null, $attribute_key = null,
+                                $parent = DATATREE_ROOT, $allLevels = true)
+    {
+        // Build the name/key filter.
+        $where = '';
+        if (!is_null($attribute_name)) {
+            $where .= 'a.attribute_name = ' . $this->_db->quote($attribute_name);
+        }
+        if (!is_null($attribute_key)) {
+            if ($where) {
+                $where .= ' AND ';
+            }
+            $where .= 'a.attribute_key = ' . $this->_db->quote($attribute_key);
+        }
+
+        // Return if we have no criteria.
+        if (!$where) {
+            return PEAR::raiseError('no criteria');
+        }
+
+        // Add filtering by parent, and for one or all levels.
+        $levelQuery = '';
+        if ($parent != DATATREE_ROOT) {
+            $parts = explode(':', $parent);
+            $parents = '';
+            $pstring = '';
+            foreach ($parts as $part) {
+                $pstring .= (empty($pstring) ? '' : ':') . $part;
+                $pid = $this->getId($pstring);
+                if (is_a($pid, 'PEAR_Error')) {
+                    return $pid;
+                }
+                $parents .= ':' . $pid;
+            }
+
+            if ($allLevels) {
+                $levelQuery = sprintf('AND (datatree_parents = %s OR datatree_parents LIKE %s)',
+                                      $this->_db->quote($parents),
+                                      $this->_db->quote($parents . ':%'));
+            } else {
+                $levelQuery = sprintf('AND datatree_parents = %s',
+                                      $this->_db->quote($parents));
+            }
+        } elseif (!$allLevels) {
+            $levelQuery = "AND datatree_parents = ''";
+        }
+
+        // Build the FROM/JOIN clauses.
+        $joins = 'LEFT JOIN ' . $this->_params['table'] .
+            ' c ON a.datatree_id = c.datatree_id';
+
+        $query = sprintf('SELECT DISTINCT a.attribute_value FROM %s a %s WHERE c.group_uid = %s AND %s %s',
+                         $this->_params['table_attributes'],
+                         $joins,
+                         $this->_db->quote($this->_params['group']),
+                         $where,
+                         $levelQuery);
+
+        Horde::logMessage('SQL Query by DataTree_sql::getAttributeValues(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $rows = $this->_db->getCol($query);
+        if (is_a($rows, 'PEAR_Error')) {
+            Horde::logMessage($rows, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $rows;
+    }
+
+    /**
+     * Builds an attribute query. Here is an example $criteria array:
+     *
+     * <code>
+     * $criteria['OR'] = array(
+     *     array('AND' => array(
+     *         array('field' => 'name',
+     *               'op'    => '=',
+     *               'test'  => 'foo'),
+     *         array('field' => 'key',
+     *               'op'    => '=',
+     *               'test'  => 'abc'))),
+     *         array('AND' => array(
+     *             array('field' => 'name',
+     *                   'op'    => '=',
+     *                   'test'  => 'bar'),
+     *             array('field' => 'key',
+     *                   'op'    => '=',
+     *                   'test'  => 'xyz'))));
+     * </code>
+     *
+     * This would fetch all object ids where attribute name is "foo" AND key
+     * is "abc", OR "bar" AND "xyz".
+     *
+     * @param array   $criteria     The array of criteria.
+     * @param string  $parent       The parent node to start searching from.
+     * @param boolean $allLevels    Return all levels, or just the direct
+     *                              children of $parent? Defaults to all levels.
+     * @param string  $restrict     Only return attributes with the same
+     *                              attribute_name or attribute_id.
+     * @param integer $operation    Type of query to build
+     * @param string  $sortby_name  Attribute name to use for sorting.
+     * @param string  $sortby_key   Attribute key to use for sorting.
+     * @param integer $direction    Sort direction:
+     *                                0 - ascending
+     *                                1 - descending
+     *
+     * @return array  An SQL query and a list of values suitable for binding
+     *                as an array.
+     */
+    function buildAttributeQuery($criteria, $parent = DATATREE_ROOT,
+                                 $allLevels = true, $restrict = 'name',
+                                 $operation = DATATREE_BUILD_SELECT,
+                                 $sortby_name = null, $sortby_key = null,
+                                 $direction = 0)
+    {
+        if (!count($criteria)) {
+            return array('', array());
+        }
+
+        /* Build the query. */
+        $this->_tableCount = 1;
+        $query = '';
+        $values = array();
+        foreach ($criteria as $key => $vals) {
+            if ($key == 'OR' || $key == 'AND') {
+                if (!empty($query)) {
+                    $query .= ' ' . $key . ' ';
+                }
+                $binds = $this->_buildAttributeQuery($key, $vals);
+                $query .= '(' . $binds[0] . ')';
+                $values += $binds[1];
+            }
+        }
+
+        // Add filtering by parent, and for one or all levels.
+        $levelQuery = '';
+        $levelValues = array();
+        if ($parent != DATATREE_ROOT) {
+            $parts = explode(':', $parent);
+            $parents = '';
+            $pstring = '';
+            foreach ($parts as $part) {
+                $pstring .= (empty($pstring) ? '' : ':') . $part;
+                $pid = $this->getId($pstring);
+                if (is_a($pid, 'PEAR_Error')) {
+                    return $pid;
+                }
+                $parents .= ':' . $pid;
+            }
+
+            if ($allLevels) {
+                $levelQuery = 'AND (datatree_parents = ? OR datatree_parents LIKE ?)';
+                $levelValues = array($parents, $parents . ':%');
+            } else {
+                $levelQuery = 'AND datatree_parents = ?';
+                $levelValues = array($parents);
+            }
+        } elseif (!$allLevels) {
+            $levelQuery = "AND datatree_parents = ''";
+        }
+
+        // Build the FROM/JOIN clauses.
+        $joins = array();
+        $pairs = array();
+        for ($i = 1; $i <= $this->_tableCount; $i++) {
+            $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] .
+                ' a' . $i . ' ON a' . $i . '.datatree_id = c.datatree_id';
+
+            if ($i != 1) {
+                if ($restrict == 'name') {
+                    $pairs[] = 'AND a1.attribute_name = a' . $i . '.attribute_name';
+                } elseif ($restrict == 'id') {
+                    $pairs[] = 'AND a1.datatree_id = a' . $i . '.datatree_id';
+                }
+            }
+        }
+
+        // Override sorting.
+        $sort = array();
+        if (!is_null($sortby_name) || !is_null($sortby_key)) {
+            $order_table = 'a' . $i;
+            $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] .
+                ' ' . $order_table . ' ON ' . $order_table .
+                '.datatree_id = c.datatree_id';
+
+            if (!is_null($sortby_name)) {
+                $pairs[] = sprintf('AND %s.attribute_name = ? ', $order_table);
+                $sort[] = $sortby_name;
+            }
+            if (!is_null($sortby_key)) {
+                $pairs[] = sprintf('AND %s.attribute_key = ? ', $order_table);
+                $sort[] = $sortby_key;
+            }
+
+            $order = sprintf('%s.attribute_value %s',
+                             $order_table,
+                             ($direction == 1) ? 'DESC' : 'ASC');
+            $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order, ' .
+                $order_table . '.attribute_value';
+        } else {
+            $order = 'c.datatree_order, c.datatree_name, c.datatree_id';
+            $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order';
+        }
+
+        $joins = implode(' ', $joins);
+        $pairs = implode(' ', $pairs);
+
+        switch ($operation) {
+
+        case DATATREE_BUILD_VALUES_COUNT:
+            $what = 'COUNT(DISTINCT(a.attribute_value))';
+            $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table'];
+            $tail = '';
+            break;
+
+        case DATATREE_BUILD_VALUES:
+            $what = 'DISTINCT(a.attribute_value)';
+            $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table'];
+            $tail = '';
+            break;
+
+        case DATATREE_BUILD_COUNT:
+            $what = 'COUNT(DISTINCT c.datatree_id)';
+            $from = $this->_params['table'];
+            $tail = '';
+            break;
+
+        default:
+            $what = 'c.datatree_id, c.datatree_name';
+            $from = $this->_params['table'];
+            $tail = sprintf('GROUP BY %s ORDER BY %s', $group_by, $order);
+            break;
+        }
+
+        return array(sprintf('SELECT %s FROM %s c %s WHERE c.group_uid = ? AND %s %s %s %s',
+                             $what,
+                             $from,
+                             $joins,
+                             $query,
+                             $levelQuery,
+                             $pairs,
+                             $tail),
+                     array_merge(array($this->_params['group']),
+                                 $values,
+                                 $levelValues,
+                                 $sort));
+    }
+
+    /**
+     * Builds a piece of an attribute query.
+     *
+     * @param string $glue     The glue to join the criteria (OR/AND).
+     * @param array $criteria  The array of criteria.
+     * @param boolean $join    Should we join on a clean
+     *                         horde_datatree_attributes table? Defaults to
+     *                         false.
+     *
+     * @return array  An SQL fragment and a list of values suitable for binding
+     *                as an array.
+     */
+    function _buildAttributeQuery($glue, $criteria, $join = false)
+    {
+        require_once 'Horde/SQL.php';
+
+        // Initialize the clause that we're building.
+        $clause = '';
+        $values = array();
+
+        // Get the table alias to use for this set of criteria.
+        $alias = $this->_getAlias($join);
+
+        foreach ($criteria as $key => $vals) {
+            if (!empty($clause)) {
+                $clause .= ' ' . $glue . ' ';
+            }
+            if (!empty($vals['OR']) || !empty($vals['AND'])) {
+                $binds = $this->_buildAttributeQuery($glue, $vals);
+                $clause .= '(' . $binds[0] . ')';
+                $values = array_merge($values, $binds[1]);
+            } elseif (!empty($vals['JOIN'])) {
+                $binds = $this->_buildAttributeQuery($glue, $vals['JOIN'], true);
+                $clause .= $binds[0];
+                $values = array_merge($values, $binds[1]);
+            } else {
+                if (isset($vals['field'])) {
+                    // All of the attribute_* fields are text, so make
+                    // sure we send strings to the database.
+                    if (is_array($vals['test'])) {
+                        for ($i = 0, $iC = count($vals['test']); $i < $iC; ++$i) {
+                            $vals['test'][$i] = (string)$vals['test'][$i];
+                        }
+                    } else {
+                        $vals['test'] = (string)$vals['test'];
+                    }
+
+                    $binds = Horde_SQL::buildClause($this->_db, $alias . '.attribute_' . $vals['field'], $vals['op'], $vals['test'], true);
+                    $clause .= $binds[0];
+                    $values = array_merge($values, $binds[1]);
+                } else {
+                    $binds = $this->_buildAttributeQuery($key, $vals);
+                    $clause .= $binds[0];
+                    $values = array_merge($values, $binds[1]);
+                }
+            }
+        }
+
+        return array($clause, $values);
+    }
+
+    /**
+     * Get an alias to horde_datatree_attributes, incrementing it if
+     * necessary.
+     *
+     * @param boolean $increment  Increment the alias count? Defaults to no.
+     */
+    function _getAlias($increment = false)
+    {
+        static $seen = array();
+
+        if ($increment && !empty($seen[$this->_tableCount])) {
+            $this->_tableCount++;
+        }
+
+        $seen[$this->_tableCount] = true;
+        return 'a' . $this->_tableCount;
+    }
+
+    /**
+     * Update the data in an object. Does not change the object's
+     * parent or name, just serialized data or attributes.
+     *
+     * @param DataTree $object  A DataTree object.
+     */
+    function updateData($object)
+    {
+        if (!is_a($object, 'DataTreeObject')) {
+            /* Nothing to do for non objects. */
+            return true;
+        }
+
+        /* Get the object id. */
+        $id = $this->getId($object->getName());
+        if (is_a($id, 'PEAR_Error')) {
+            return $id;
+        }
+
+        /* See if we can break the object out to datatree_attributes table. */
+        if (method_exists($object, '_toAttributes')) {
+            /* If we can, clear out the datatree_data field to make sure it
+             * doesn't get picked up by getData(). Intentionally don't check
+             * for errors here in case datatree_data goes away in the
+             * future. */
+            $query = 'UPDATE ' . $this->_params['table'] .
+                     ' SET datatree_data = ? WHERE datatree_id = ?';
+            $values = array(NULL, (int)$id);
+
+            Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_write_db->query($query, $values);
+
+            /* Start a transaction. */
+            $this->_write_db->autoCommit(false);
+
+            /* Delete old attributes. */
+            $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
+                     ' WHERE datatree_id = ?';
+            $values = array((int)$id);
+
+            Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $result = $this->_write_db->query($query, $values);
+            if (is_a($result, 'PEAR_Error')) {
+                $this->_write_db->rollback();
+                $this->_write_db->autoCommit(true);
+                return $result;
+            }
+
+            /* Get the new attribute set, and insert each into the DB. If
+             * anything fails in here, rollback the transaction, return the
+             * relevant error, and bail out. */
+            $attributes = $object->_toAttributes();
+            $query = 'INSERT INTO ' . $this->_params['table_attributes'] .
+                     ' (datatree_id, attribute_name, attribute_key, attribute_value)' .
+                     ' VALUES (?, ?, ?, ?)';
+            $statement = $this->_write_db->prepare($query);
+            foreach ($attributes as $attr) {
+                $values = array((int)$id,
+                                $attr['name'],
+                                $attr['key'],
+                                Horde_String::convertCharset($attr['value'], Horde_Nls::getCharset(), $this->_params['charset']));
+
+                Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                $result = $this->_write_db->execute($statement, $values);
+                if (is_a($result, 'PEAR_Error')) {
+                    $this->_write_db->rollback();
+                    $this->_write_db->autoCommit(true);
+                    return $result;
+                }
+            }
+
+            /* Commit the transaction, and turn autocommit back on. */
+            $result = $this->_write_db->commit();
+            $this->_write_db->autoCommit(true);
+
+            return is_a($result, 'PEAR_Error') ? $result : true;
+        } else {
+            /* Write to the datatree_data field. */
+            require_once 'Horde/Serialize.php';
+            $ser = Horde_Serialize::UTF7_BASIC;
+            $data = Horde_Serialize::serialize($object->getData(), $ser, Horde_Nls::getCharset());
+
+            $query = 'UPDATE ' . $this->_params['table'] .
+                     ' SET datatree_data = ?, datatree_serialized = ?' .
+                     ' WHERE datatree_id = ?';
+            $values = array($data,
+                            (int)$ser,
+                            (int)$id);
+
+            Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $result = $this->_write_db->query($query, $values);
+
+            return is_a($result, 'PEAR_Error') ? $result : true;
+        }
+    }
+
+    /**
+     * Attempts to open a connection to the SQL server.
+     *
+     * @return boolean  True.
+     */
+    function _init()
+    {
+        Horde::assertDriverConfig($this->_params, 'sql',
+            array('phptype', 'charset'),
+            'DataTree SQL');
+
+        $default = array(
+            'database' => '',
+            'username' => '',
+            'password' => '',
+            'hostspec' => '',
+            'table' => 'horde_datatree',
+            'table_attributes' => 'horde_datatree_attributes',
+        );
+        $this->_params = array_merge($default, $this->_params);
+
+        /* Connect to the SQL server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_write_db = DB::connect($this->_params,
+                                       array('persistent' => !empty($this->_params['persistent']),
+                                             'ssl' => !empty($this->_params['ssl'])));
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            return $this->_write_db;
+        }
+
+        // Set DB portability options.
+        $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS;
+        if ($this->_write_db->phptype == 'mssql') {
+            $portability |= DB_PORTABILITY_RTRIM;
+        }
+        $this->_write_db->setOption('portability', $portability);
+
+        /* Check if we need to set up the read DB connection
+         * seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = DB::connect($params,
+                                     array('persistent' => !empty($params['persistent']),
+                                           'ssl' => !empty($params['ssl'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                return $this->_db;
+            }
+
+            // Set DB portability options
+            $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS;
+            if ($this->_db->phptype == 'mssql') {
+                $portability |= DB_PORTABILITY_RTRIM;
+            }
+            $this->_db->setOption('portability', $portability);
+        } else {
+            /* Default to the same DB handle for reads. */
+            $this->_db = $this->_write_db;
+        }
+
+        return true;
+    }
+
+}
diff --git a/framework/DataTree/docs/find-datatree-attribute-orphans.sql b/framework/DataTree/docs/find-datatree-attribute-orphans.sql
new file mode 100644 (file)
index 0000000..6e2962c
--- /dev/null
@@ -0,0 +1 @@
+SELECT DISTINCT da.datatree_id FROM horde_datatree_attributes da LEFT JOIN horde_datatree d ON da.datatree_id = d.datatree_id WHERE d.datatree_id IS NULL
diff --git a/framework/DataTree/package.xml b/framework/DataTree/package.xml
new file mode 100644 (file)
index 0000000..3fb9840
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_DataTree</name>
+ <channel>pear.horde.org</channel>
+ <summary>DataTree API</summary>
+ <description>TODO
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>21:17:43</time>
+ <version>
+  <release>0.0.3</release>
+  <api>0.0.3</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="DataTree">
+    <file baseinstalldir="/Horde" name="null.php" role="php" />
+    <file baseinstalldir="/Horde" name="sql.php" role="php" />
+   </dir> <!-- //DataTree -->
+   <file baseinstalldir="/Horde" name="DataTree.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Serialize</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_SQL</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Add failover functionality, if the sql driver is not available will fall back to null driver.
+* Add support for separate read and write DB servers for the sql driver.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-02-11</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial packaging.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/File_CSV/CSV.php b/framework/File_CSV/CSV.php
new file mode 100644 (file)
index 0000000..77bb811
--- /dev/null
@@ -0,0 +1,571 @@
+<?php
+/**
+ * @package File_CSV
+ */
+
+/** PEAR */
+require_once 'PEAR.php';
+
+/** Mode to use for reading from files */
+define('HORDE_FILE_CSV_MODE_READ', 'rb');
+
+/** Mode to use for truncating files, then writing */
+define('HORDE_FILE_CSV_MODE_WRITE', 'wb');
+
+/** Mode to use for appending to files */
+define('HORDE_FILE_CSV_MODE_APPEND', 'ab');
+
+/**
+ * The File_CSV package allows reading and creating of CSV data and files.
+ *
+ * $Horde: framework/File_CSV/CSV.php,v 1.25 2009/01/06 17:49:15 jan Exp $
+ *
+ * Copyright 2002-2003 Tomas Von Veschler Cox <cox@idecnet.com>
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * This source file is subject to version 2.0 of the PHP license, that is
+ * bundled with this package in the file LICENSE, and is available at through
+ * the world-wide-web at http://www.php.net/license/2_02.txt.  If you did not
+ * receive a copy of the PHP license and are unable to obtain it through the
+ * world-wide-web, please send a note to license@php.net so we can mail you a
+ * copy immediately.
+ *
+ * @author  Tomas Von Veschler Cox <cox@idecnet.com>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.1
+ * @package File_CSV
+ */
+class File_CSV {
+
+    /**
+     * Discovers the format of a CSV file (the number of fields, the separator,
+     * the quote string, and the line break).
+     *
+     * We can't use the auto_detect_line_endings PHP setting, because it's not
+     * supported by fgets() contrary to what the manual says.
+     *
+     * @static
+     *
+     * @param string  The CSV file name
+     * @param array   Extra separators that should be checked for.
+     *
+     * @return array  The format hash.
+     */
+    function discoverFormat($file, $extraSeps = array())
+    {
+        if (!$fp = @fopen($file, 'r')) {
+            return PEAR::raiseError('Could not open file: ' . $file);
+        }
+
+        $seps = array("\t", ';', ':', ',', '~');
+        $seps = array_merge($seps, $extraSeps);
+        $matches = array();
+        $crlf = null;
+        $conf = array();
+
+        /* Take the first 10 lines and store the number of ocurrences for each
+         * separator in each line. */
+        for ($i = 0; ($i < 10) && ($line = fgets($fp));) {
+            /* Do we have Mac line endings? */
+            $lines = preg_split('/\r(?!\n)/', $line, 10);
+            $j = 0;
+            $c = count($lines);
+            if ($c > 1) {
+                $crlf = "\r";
+            }
+            while ($i < 10 && $j < $c) {
+                $line = $lines[$j];
+                if (!isset($crlf)) {
+                    foreach (array("\r\n", "\n") as $c) {
+                        if (substr($line, -strlen($c)) == $c) {
+                            $crlf = $c;
+                            break;
+                        }
+                    }
+                }
+                $i++;
+                $j++;
+                foreach ($seps as $sep) {
+                    $matches[$sep][$i] = substr_count($line, $sep);
+                }
+            }
+        }
+        if (isset($crlf)) {
+            $conf['crlf'] = $crlf;
+        }
+
+        /* Group the results by amount of equal occurrences. */
+        $fields = array();
+        $amount = array();
+        foreach ($matches as $sep => $lines) {
+            $times = array();
+            $times[0] = 0;
+            foreach ($lines as $num) {
+                if ($num > 0) {
+                    $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1;
+                }
+            }
+            arsort($times);
+            $fields[$sep] = key($times);
+            $amount[$sep] = $times[key($times)];
+        }
+        arsort($amount);
+        $sep = key($amount);
+
+        $conf['fields'] = $fields[$sep] + 1;
+        $conf['sep']    = $sep;
+
+        /* Test if there are fields with quotes around in the first 10
+         * lines. */
+        $quotes = '"\'';
+        $quote  = '';
+        rewind($fp);
+        for ($i = 0; ($i < 10) && ($line = fgets($fp)); $i++) {
+            if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) {
+                if ($match[1] == $match[2]) {
+                    $quote = $match[1];
+                    break;
+                }
+            }
+            if (preg_match("|^([$quotes]).*([$quotes])$sep|", $line, $match) ||
+                preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match)) {
+                if ($match[1] == $match[2]) {
+                    $quote = $match[1];
+                    break;
+                }
+            }
+        }
+        $conf['quote'] = $quote;
+
+        fclose($fp);
+
+        // XXX What about trying to discover the "header"?
+        return $conf;
+    }
+
+    /**
+     * Reads a row from a CSV file and returns it as an array.
+     *
+     * This method normalizes linebreaks to single newline characters (0x0a).
+     *
+     * @param string $file  The name of the CSV file.
+     * @param array $conf   The configuration for the CSV file.
+     *
+     * @return array|boolean  The CSV data or false if no more data available.
+     */
+    function read($file, &$conf)
+    {
+        $fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_READ);
+        if (is_a($fp, 'PEAR_Error')) {
+            return $fp;
+        }
+
+        $line = fgets($fp);
+        $line_length = strlen($line);
+
+        /* Use readQuoted() if we have Mac line endings. */
+        if (preg_match('/\r(?!\n)/', $line)) {
+            fseek($fp, -$line_length, SEEK_CUR);
+            return File_CSV::readQuoted($file, $conf);
+        }
+
+        /* Normalize line endings. */
+        $line = str_replace("\r\n", "\n", $line);
+        if (!strlen(trim($line))) {
+            return false;
+        }
+
+        File_CSV::_line(File_CSV::_line() + 1);
+
+        if ($conf['fields'] == 1) {
+            return array($line);
+        }
+
+        $fields = explode($conf['sep'], $line);
+        if ($conf['quote']) {
+            $last = ltrim($fields[count($fields) - 1]);
+            /* Fallback to read the line with readQuoted() if we assume that
+             * the simple explode won't work right. */
+            $last_len = strlen($last);
+            if (($last_len &&
+                 $last[$last_len - 1] == "\n" &&
+                 $last[0] == $conf['quote'] &&
+                 $last[strlen(rtrim($last)) - 1] != $conf['quote']) ||
+                (count($fields) != $conf['fields'])
+                // XXX perhaps there is a separator inside a quoted field
+                // preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
+                ) {
+                fseek($fp, -$line_length, SEEK_CUR);
+                return File_CSV::readQuoted($file, $conf);
+            } else {
+                foreach ($fields as $k => $v) {
+                    $fields[$k] = File_CSV::unquote(trim($v), $conf['quote']);
+                }
+            }
+        } else {
+            foreach ($fields as $k => $v) {
+                $fields[$k] = trim($v);
+            }
+        }
+
+        if (count($fields) < $conf['fields']) {
+            File_CSV::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), File_CSV::_line(), $conf['fields'], count($fields)));
+            $fields = array_merge($fields, array_fill(0, $conf['fields'] - count($fields), ''));
+        } elseif (count($fields) > $conf['fields']) {
+            File_CSV::warning(sprintf(_("More fields found in line %d than the expected %d."), File_CSV::_line(), $conf['fields']));
+            array_splice($fields, $conf['fields']);
+        }
+
+        return $fields;
+    }
+
+    /**
+     * Reads a row from a CSV file and returns it as an array.
+     *
+     * This method is able to read fields with multiline data and normalizes
+     * linebreaks to single newline characters (0x0a).
+     *
+     * @param string $file  The name of the CSV file.
+     * @param array $conf   The configuration for the CSV file.
+     *
+     * @return array|boolean  The CSV data or false if no more data available.
+     */
+    function readQuoted($file, &$conf)
+    {
+        $fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_READ);
+        if (is_a($fp, 'PEAR_Error')) {
+            return $fp;
+        }
+
+        /* A buffer with all characters of the current field read so far. */
+        $buff = '';
+        /* The current character. */
+        $c = null;
+        /* The read fields. */
+        $ret = false;
+        /* The number of the current field. */
+        $i = 0;
+        /* Are we inside a quoted field? */
+        $in_quote = false;
+        /* Did we just process an escaped quote? */
+        $quote_escaped = false;
+        /* Is the last processed quote the first of a field? */
+        $first_quote = false;
+
+        while (($ch = fgetc($fp)) !== false) {
+            /* Normalize line breaks. */
+            if ($ch == $conf['crlf']) {
+                $ch = "\n";
+            } elseif (strlen($conf['crlf']) == 2 && $ch == $conf['crlf'][0]) {
+                $next = fgetc($fp);
+                if (!$next) {
+                    break;
+                }
+                if ($next == $conf['crlf'][1]) {
+                    $ch = "\n";
+                }
+            }
+
+            /* Previous character. */
+            $prev = $c;
+            /* Current character. */
+            $c = $ch;
+
+            /* Simple character. */
+            if ($c != $conf['quote'] &&
+                $c != $conf['sep'] &&
+                $c != "\n") {
+                $buff .= $c;
+                if (!$i) {
+                    $i = 1;
+                }
+                $quote_escaped = false;
+                $first_quote = false;
+                continue;
+            }
+
+            if ($c == $conf['quote'] && !$in_quote) {
+                /* Quoted field begins. */
+                $in_quote = true;
+                $buff = '';
+                if (!$i) {
+                    $i = 1;
+                }
+            } elseif ($in_quote) {
+                /* We do NOT check for the closing quote immediately, but when
+                 * we got the character AFTER the closing quote. */
+                if ($c == $conf['quote'] && $prev == $conf['quote'] &&
+                    !$quote_escaped) {
+                    /* Escaped (double) quotes. */
+                    $quote_escaped = true;
+                    $first_quote = true;
+                    $prev = null;
+                    /* Simply skip the second quote. */
+                    continue;
+                } elseif ($c == $conf['sep'] && $prev == $conf['quote']) {
+                    /* Quoted field ends with a delimiter. */
+                    $in_quote = false;
+                    $quote_escaped = false;
+                    $first_quote = true;
+                } elseif ($c == "\n") {
+                    /* We have a linebreak inside the quotes. */
+                    if (strlen($buff) == 1 &&
+                        $buff[0] == $conf['quote'] &&
+                        $quote_escaped && $first_quote) {
+                        /* A line break after a closing quote of an empty
+                         * field, field and row end here. */
+                        $in_quote = false;
+                    } elseif (strlen($buff) >= 1 &&
+                        $buff[strlen($buff) - 1] == $conf['quote'] &&
+                        !$quote_escaped && !$first_quote) {
+                        /* A line break after a closing quote, field and row
+                         * end here. This is NOT the closing quote if we
+                         * either process an escaped (double) quote, or if the
+                         * quote before the line break was the opening
+                         * quote. */
+                        $in_quote = false;
+                    } else {
+                        /* Only increment the line number. Line breaks inside
+                         * quoted fields are part of the field content. */
+                        File_CSV::_line(File_CSV::_line() + 1);
+                    }
+                    $quote_escaped = false;
+                    $first_quote = true;
+                }
+            }
+
+            if (!$in_quote &&
+                ($c == $conf['sep'] || $c == "\n")) {
+                /* End of line or end of field. */
+                if ($c == $conf['sep'] &&
+                    (count($ret) + 1) == $conf['fields']) {
+                    /* More fields than expected. Forward the line pointer to
+                     * the EOL and drop the remainder. */
+                    while ($c !== false && $c != "\n") {
+                        $c = fgetc($fp);
+                    }
+                    File_CSV::warning(sprintf(_("More fields found in line %d than the expected %d."), File_CSV::_line(), $conf['fields']));
+                }
+
+                if ($c == "\n" &&
+                    $i != $conf['fields']) {
+                    /* Less fields than expected. */
+                    if ($i == 0) {
+                        /* Skip empty lines. */
+                        return $ret;
+                    }
+                    File_CSV::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), File_CSV::_line(), $conf['fields'], $i));
+
+                    $ret[] = File_CSV::unquote($buff, $conf['quote']);
+                    $ret = array_merge($ret, array_fill(0, $conf['fields'] - $i, ''));
+                    return $ret;
+                }
+
+                /* Remove surrounding quotes from quoted fields. */
+                if ($buff == '"') {
+                    $ret[] = '';
+                } else {
+                    $ret[] = File_CSV::unquote($buff, $conf['quote']);
+                }
+                if (count($ret) == $conf['fields']) {
+                    return $ret;
+                }
+
+                $buff = '';
+                $i++;
+                continue;
+            }
+            $buff .= $c;
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Writes a hash into a CSV file.
+     *
+     * @param string $file   The name of the CSV file.
+     * @param array $fields  The CSV data.
+     * @param array $conf    The configuration for the CSV file.
+     *
+     * @return boolean  True on success, PEAR_Error on failure.
+     */
+    function write($file, $fields, &$conf)
+    {
+        if (is_a($fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_WRITE), 'PEAR_Error')) {
+            return $fp;
+        }
+
+        if (count($fields) != $conf['fields']) {
+            return PEAR::raiseError(sprintf(_("Wrong number of fields. Expected %d, found %d."), $conf['fields'], count($fields)));
+        }
+
+        $write = '';
+        for ($i = 0; $i < count($fields); $i++) {
+            if (!is_numeric($fields[$i]) && $conf['quote']) {
+                $write .= $conf['quote'] . $fields[$i] . $conf['quote'];
+            } else {
+                $write .= $fields[$i];
+            }
+            if ($i < (count($fields) - 1)) {
+                $write .= $conf['sep'];
+            } else {
+                $write .= $conf['crlf'];
+            }
+        }
+
+        if (!fwrite($fp, $write)) {
+            return PEAR::raiseError(sprintf(_("Cannot write to file \"%s\""), $file));
+        }
+
+        return true;
+    }
+
+    /**
+     * Removes surrounding quotes from a string and normalizes linebreaks.
+     *
+     * @param string $field  The string to unquote.
+     * @param string $quote  The quote character.
+     * @param string $crlf   The linebreak character.
+     *
+     * @return string  The unquoted data.
+     */
+    function unquote($field, $quote, $crlf = null)
+    {
+        /* Skip empty fields (form: ;;) */
+        if (!strlen($field)) {
+            return $field;
+        }
+        if ($quote && $field[0] == $quote &&
+            $field[strlen($field) - 1] == $quote) {
+            /* Normalize only for BC. */
+            if ($crlf) {
+                $field = str_replace($crlf, "\n", $field);
+            }
+            return substr($field, 1, -1);
+        }
+        return $field;
+    }
+
+    /**
+     * Sets or gets the current line being parsed.
+     *
+     * @param integer $line  If specified, the current line.
+     *
+     * @return integer  The current line.
+     */
+    function _line($line = null)
+    {
+        static $current_line = 0;
+
+        if (!is_null($line)) {
+            $current_line = $line;
+        }
+
+        return $current_line;
+    }
+
+    /**
+     * Adds a warning to or retrieves and resets the warning stack.
+     *
+     * @param string  A warning string.  If not specified, the existing
+     *                warnings will be returned instead and the warning stack
+     *                gets emptied.
+     *
+     * @return array  If no parameter has been specified, the list of existing
+     *                warnings.
+     */
+    function warning($warning = null)
+    {
+        static $warnings = array();
+
+        if (is_null($warning)) {
+            $return = $warnings;
+            $warnings = array();
+            return $return;
+        }
+
+        $warnings[] = $warning;
+    }
+
+    /**
+     * Returns or creates the file descriptor associated with a file.
+     *
+     * @static
+     *
+     * @param string $file  The name of the file
+     * @param array $conf   The configuration
+     * @param string $mode  The open mode. HORDE_FILE_CSV_MODE_READ or
+     *                      HORDE_FILE_CSV_MODE_WRITE.
+     *
+     * @return resource  The file resource or PEAR_Error on error.
+     */
+    function getPointer($file, &$conf, $mode = HORDE_FILE_CSV_MODE_READ)
+    {
+        static $resources = array();
+        static $config = array();
+
+        if (isset($resources[$file])) {
+            $conf = $config[$file];
+            return $resources[$file];
+        }
+        if (is_a($error = File_CSV::_checkConfig($conf), 'PEAR_Error')) {
+            return $error;
+        }
+        $config[$file] = $conf;
+
+        $fp = @fopen($file, $mode);
+        if (!is_resource($fp)) {
+            return PEAR::raiseError(sprintf(_("Cannot open file \"%s\"."), $file));
+        }
+        $resources[$file] = $fp;
+        File_CSV::_line(0);
+
+        if ($mode == HORDE_FILE_CSV_MODE_READ && !empty($conf['header'])) {
+            if (is_a($header = File_CSV::read($file, $conf), 'PEAR_Error')) {
+                return $header;
+            }
+        }
+
+        return $fp;
+    }
+
+    /**
+     * Checks the configuration given by the user.
+     *
+     * @param array $conf    The configuration assoc array
+     * @param string $error  The error will be written here if any
+     */
+    function _checkConfig(&$conf)
+    {
+        // check conf
+        if (!is_array($conf)) {
+            return PEAR::raiseError('Invalid configuration.');
+        }
+
+        if (!isset($conf['fields']) || !is_numeric($conf['fields'])) {
+            return PEAR::raiseError(_("The number of fields must be numeric."));
+        }
+
+        if (isset($conf['sep'])) {
+            if (strlen($conf['sep']) != 1) {
+                return PEAR::raiseError(_("The separator must be one single character."));
+            }
+        } elseif ($conf['fields'] > 1) {
+            return PEAR::raiseError(_("No separator specified."));
+        }
+
+        if (!empty($conf['quote'])) {
+            if (strlen($conf['quote']) != 1) {
+                return PEAR::raiseError(_("The quote character must be one single character."));
+            }
+        } else {
+            $conf['quote'] = '';
+        }
+
+        if (!isset($conf['crlf'])) {
+            $conf['crlf'] = "\n";
+        }
+    }
+
+}
diff --git a/framework/File_CSV/package.xml b/framework/File_CSV/package.xml
new file mode 100644 (file)
index 0000000..d898fb3
--- /dev/null
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>File_CSV</name>
+ <channel>pear.horde.org</channel>
+ <summary>Reads and writes CSV files</summary>
+ <description>The File_CSV package allows reading and creating of CSV data and files. It
+is a fork of the File_CSV class of PEAR&apos;s File package.
+ </description>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>21:32:31</time>
+ <version>
+  <release>0.1.1</release>
+  <api>0.1.1</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.php.net/license">PHP</license>
+ <notes>
+* Converted to package.xml 2.0 for pear.horde.org
+* Close Horde bug #6372 (ritaselsky@gmail.com)
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="tests">
+    <file name="bug_3839.csv" role="test" />
+    <file name="bug_3839.phpt" role="test" />
+    <file name="columns.phpt" role="test" />
+    <file name="columns1.csv" role="test" />
+    <file name="common.php" role="test" />
+    <file name="multiline.phpt" role="test" />
+    <file name="multiline1.csv" role="test" />
+    <file name="notrailing_crlf.csv" role="test" />
+    <file name="notrailing_lf.csv" role="test" />
+    <file name="quote1.csv" role="test" />
+    <file name="quote2.csv" role="test" />
+    <file name="quote3.csv" role="test" />
+    <file name="quote4.csv" role="test" />
+    <file name="quote5.csv" role="test" />
+    <file name="quotes.phpt" role="test" />
+    <file name="simple_cr.csv" role="test" />
+    <file name="simple_crlf.csv" role="test" />
+    <file name="simple_lf.csv" role="test" />
+   </dir> <!-- //tests -->
+   <file baseinstalldir="/File" name="CSV.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>PEAR</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <extension>
+    <name>pcre</name>
+   </extension>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-01-01</date>
+   <license uri="http://www.php.net/license">PHP</license>
+   <notes>First release.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/File_CSV/tests/001.csv b/framework/File_CSV/tests/001.csv
new file mode 100755 (executable)
index 0000000..d217fb0
--- /dev/null
@@ -0,0 +1,4 @@
+"Field 1-1", "Field 1-2", "Field 1-3", "Field 1-4"
+"Field 2-1", "Field 2-2", "Field 2-3"
+"Field 3-1", "Field 3-2"
+"Field 4-1"
diff --git a/framework/File_CSV/tests/001.phpt b/framework/File_CSV/tests/001.phpt
new file mode 100755 (executable)
index 0000000..33ff355
--- /dev/null
@@ -0,0 +1,84 @@
+--TEST--
+File_CSV Test Case 001: Fields count less than expected
+--FILE--
+<?php
+/**
+ * Test for:
+ * - File_CSV::discoverFormat()
+ * - File_CSV::readQuoted()
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/001.csv';
+$conf = File_CSV::discoverFormat($file);
+$conf['fields'] = 4;
+
+var_dump($conf);
+
+$data = array();
+while ($res = File_CSV::readQuoted($file, $conf)) {
+    $data[] = $res;
+}
+
+var_dump($data);
+
+?>
+--EXPECT--
+array(4) {
+  ["crlf"]=>
+  string(1) "
+"
+  ["fields"]=>
+  int(4)
+  ["sep"]=>
+  string(1) ","
+  ["quote"]=>
+  string(1) """
+}
+array(4) {
+  [0]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 1-1"
+    [1]=>
+    string(9) "Field 1-2"
+    [2]=>
+    string(9) "Field 1-3"
+    [3]=>
+    string(9) "Field 1-4"
+  }
+  [1]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 2-1"
+    [1]=>
+    string(9) "Field 2-2"
+    [2]=>
+    string(9) "Field 2-3"
+    [3]=>
+    string(0) ""
+  }
+  [2]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 3-1"
+    [1]=>
+    string(9) "Field 3-2"
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+  }
+  [3]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 4-1"
+    [1]=>
+    string(0) ""
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+  }
+}
diff --git a/framework/File_CSV/tests/002.csv b/framework/File_CSV/tests/002.csv
new file mode 100755 (executable)
index 0000000..bcd6ce0
--- /dev/null
@@ -0,0 +1,4 @@
+"Field 1-1", "Field 1-2", "Field 1-3", "Field 1-4"
+"Field 2-1", "Field 2-2", "Field 2-3", "Field 2-4", "Extra Field"
+"Field 3-1", "Field 3-2"
+"Field 4-1"
diff --git a/framework/File_CSV/tests/002.phpt b/framework/File_CSV/tests/002.phpt
new file mode 100755 (executable)
index 0000000..ccc5367
--- /dev/null
@@ -0,0 +1,84 @@
+--TEST--
+File_CSV Test Case 002: Fields count more than expected
+--FILE--
+<?php
+/**
+ * Test for:
+ * - File_CSV::discoverFormat()
+ * - File_CSV::readQuoted()
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/002.csv';
+$conf = File_CSV::discoverFormat($file);
+$conf['fields'] = 4;
+
+var_dump($conf);
+
+$data = array();
+while ($res = File_CSV::readQuoted($file, $conf)) {
+    $data[] = $res;
+}
+
+var_dump($data);
+
+?>
+--EXPECT--
+array(4) {
+  ["crlf"]=>
+  string(1) "
+"
+  ["fields"]=>
+  int(4)
+  ["sep"]=>
+  string(1) ","
+  ["quote"]=>
+  string(1) """
+}
+array(4) {
+  [0]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 1-1"
+    [1]=>
+    string(9) "Field 1-2"
+    [2]=>
+    string(9) "Field 1-3"
+    [3]=>
+    string(9) "Field 1-4"
+  }
+  [1]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 2-1"
+    [1]=>
+    string(9) "Field 2-2"
+    [2]=>
+    string(9) "Field 2-3"
+    [3]=>
+    string(9) "Field 2-4"
+  }
+  [2]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 3-1"
+    [1]=>
+    string(9) "Field 3-2"
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+  }
+  [3]=>
+  array(4) {
+    [0]=>
+    string(9) "Field 4-1"
+    [1]=>
+    string(0) ""
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+  }
+}
diff --git a/framework/File_CSV/tests/003.csv b/framework/File_CSV/tests/003.csv
new file mode 100644 (file)
index 0000000..62cb0f7
--- /dev/null
@@ -0,0 +1,4 @@
+"Field 1-1","Field 1-2","Field 1-3","Field 1-4"
+"Field 2-1","Field 2-2","Field 2-3","I'm multiline
+Field"
+"Field 3-1","Field 3-2","Field 3-3"
diff --git a/framework/File_CSV/tests/003.phpt b/framework/File_CSV/tests/003.phpt
new file mode 100644 (file)
index 0000000..2640759
--- /dev/null
@@ -0,0 +1,67 @@
+--TEST--
+File_CSV Test Case 003: Windows EOL
+--FILE--
+<?php
+/**
+ * Test for:
+ * - File_CSV::discoverFormat()
+ * - File_CSV::readQuoted()
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/003.csv';
+$conf = File_CSV::discoverFormat($file);
+
+print "Format:\n";
+print_r($conf);
+print "\n";
+
+$data = array();
+while ($res = File_CSV::readQuoted($file, $conf)) {
+    $data[] = $res;
+}
+
+print "Data:\n";
+print_r($data);
+?>
+--EXPECT--
+Format:
+Array
+(
+    [crlf] => 
+
+    [fields] => 4
+    [sep] => ,
+    [quote] => "
+)
+
+Data:
+Array
+(
+    [0] => Array
+        (
+            [0] => Field 1-1
+            [1] => Field 1-2
+            [2] => Field 1-3
+            [3] => Field 1-4
+        )
+
+    [1] => Array
+        (
+            [0] => Field 2-1
+            [1] => Field 2-2
+            [2] => Field 2-3
+            [3] => I'm multiline
+Field
+        )
+
+    [2] => Array
+        (
+            [0] => Field 3-1
+            [1] => Field 3-2
+            [2] => Field 3-3
+            [3] => 
+        )
+
+)
diff --git a/framework/File_CSV/tests/004.csv b/framework/File_CSV/tests/004.csv
new file mode 100644 (file)
index 0000000..62cb0f7
--- /dev/null
@@ -0,0 +1,4 @@
+"Field 1-1","Field 1-2","Field 1-3","Field 1-4"
+"Field 2-1","Field 2-2","Field 2-3","I'm multiline
+Field"
+"Field 3-1","Field 3-2","Field 3-3"
diff --git a/framework/File_CSV/tests/004.phpt b/framework/File_CSV/tests/004.phpt
new file mode 100644 (file)
index 0000000..e85be81
--- /dev/null
@@ -0,0 +1,67 @@
+--TEST--
+File_CSV Test Case 004: Unix EOL
+--FILE--
+<?php
+/**
+ * Test for:
+ * - File_CSV::discoverFormat()
+ * - File_CSV::readQuoted()
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/004.csv';
+$conf = File_CSV::discoverFormat($file);
+
+print "Format:\n";
+print_r($conf);
+print "\n";
+
+$data = array();
+while ($res = File_CSV::readQuoted($file, $conf)) {
+    $data[] = $res;
+}
+
+print "Data:\n";
+print_r($data);
+?>
+--EXPECT--
+Format:
+Array
+(
+    [crlf] => 
+
+    [fields] => 4
+    [sep] => ,
+    [quote] => "
+)
+
+Data:
+Array
+(
+    [0] => Array
+        (
+            [0] => Field 1-1
+            [1] => Field 1-2
+            [2] => Field 1-3
+            [3] => Field 1-4
+        )
+
+    [1] => Array
+        (
+            [0] => Field 2-1
+            [1] => Field 2-2
+            [2] => Field 2-3
+            [3] => I'm multiline
+Field
+        )
+
+    [2] => Array
+        (
+            [0] => Field 3-1
+            [1] => Field 3-2
+            [2] => Field 3-3
+            [3] => 
+        )
+
+)
diff --git a/framework/File_CSV/tests/005.csv b/framework/File_CSV/tests/005.csv
new file mode 100644 (file)
index 0000000..3b26aa9
--- /dev/null
@@ -0,0 +1 @@
+"Field 1-1","Field 1-2","Field 1-3","Field 1-4"\r"Field 2-1","Field 2-2","Field 2-3","I'm multiline\rField"\r"Field 3-1","Field 3-2","Field 3-3"\r
\ No newline at end of file
diff --git a/framework/File_CSV/tests/005.phpt b/framework/File_CSV/tests/005.phpt
new file mode 100644 (file)
index 0000000..78701c0
--- /dev/null
@@ -0,0 +1,67 @@
+--TEST--
+File_CSV Test Case 005: Mac EOL
+--FILE--
+<?php
+/**
+ * Test for:
+ * - File_CSV::discoverFormat()
+ * - File_CSV::readQuoted()
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/005.csv';
+$conf = File_CSV::discoverFormat($file);
+//$conf['fields'] = 4;
+
+print "Format:\n";
+print_r($conf);
+print "\n";
+
+$data = array();
+while ($res = File_CSV::readQuoted($file, $conf)) {
+    $data[] = $res;
+}
+
+print "Data:\n";
+print_r($data);
+?>
+--EXPECT--
+Format:
+Array
+(
+    [crlf] => 
+    [fields] => 4
+    [sep] => ,
+    [quote] => "
+)
+
+Data:
+Array
+(
+    [0] => Array
+        (
+            [0] => Field 1-1
+            [1] => Field 1-2
+            [2] => Field 1-3
+            [3] => Field 1-4
+        )
+
+    [1] => Array
+        (
+            [0] => Field 2-1
+            [1] => Field 2-2
+            [2] => Field 2-3
+            [3] => I'm multiline
+Field
+        )
+
+    [2] => Array
+        (
+            [0] => Field 3-1
+            [1] => Field 3-2
+            [2] => Field 3-3
+            [3] => 
+        )
+
+)
diff --git a/framework/File_CSV/tests/bug_3839.csv b/framework/File_CSV/tests/bug_3839.csv
new file mode 100644 (file)
index 0000000..7deda38
--- /dev/null
@@ -0,0 +1,22 @@
+Subject~Start Date~Start Time~End Date~End Time~All day event~Reminder on/off~Reminder Date~Reminder Time~Category~Description~Priority
+"Inservice on new resource: ""CPNP Toolkit"""~2004-11-08~10:30 AM~2004-11-08~11:30 AM~FALSE~FALSE~~~Training~"CPN Program ... 
+Inservice on new resource: ""CPNP Toolkit""
+
+<b>Registration Deadline:  October 27, 2004, noon</b>
+
+<a href=""F041108A-Eval.pdf"" target=""_blank"">
+<img src=""acrobat.gif"" border=""0""></a>  <a href=""F041108A-Eval.pdf"" target=""_blank"">  Session Evaluation - Eligibility for Prize!</a>
+
+<a href=""F041108A-DI.pdf"" target=""_blank"">
+<img src=""acrobat.gif"" border=""0""></a>  <a href=""F041108A-DI.pdf"" target=""_blank"">  Dial In Numbers for Sites Registered</a>
+
+<a href=""F041108A.pdf"" target=""_blank"">
+<img src=""acrobat.gif"" border=""0""></a>  <a href=""F041108A.pdf"" target=""_blank"">  Poster and Registration Form</a>
+
+Facilitator:  Manager 
+
+preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb  ""CPNP Toolkit"".  postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb .
+
+postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb 
+
+Come check out the new resource!"~Normal
diff --git a/framework/File_CSV/tests/bug_3839.phpt b/framework/File_CSV/tests/bug_3839.phpt
new file mode 100644 (file)
index 0000000..4371409
--- /dev/null
@@ -0,0 +1,110 @@
+--TEST--
+File_CSV: test for Bug #3839
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/bug_3839.csv';
+
+// Explicit conf since we can't detect these settings. Might be able
+// to improve auto-detection, but it definitely should work with the
+// settings specified explicitly.
+// var_dump(File_CSV::discoverFormat($file));
+$conf['crlf'] = "\r\n";
+$conf['sep'] = '~';
+$conf['fields'] = 12;
+$conf['quote'] = '"';
+
+$csv = array();
+while ($row = File_CSV::read($file, $conf)) {
+    if (is_a($row, 'PEAR_Error')) {
+        var_dump($row);
+        return;
+    }
+    $csv[] = $row;
+}
+var_dump($csv);
+$warnings = File_CSV::warning();
+if (count($warnings)) {
+    var_dump($warnings);
+}
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(12) {
+    [0]=>
+    string(7) "Subject"
+    [1]=>
+    string(10) "Start Date"
+    [2]=>
+    string(10) "Start Time"
+    [3]=>
+    string(8) "End Date"
+    [4]=>
+    string(8) "End Time"
+    [5]=>
+    string(13) "All day event"
+    [6]=>
+    string(15) "Reminder on/off"
+    [7]=>
+    string(13) "Reminder Date"
+    [8]=>
+    string(13) "Reminder Time"
+    [9]=>
+    string(8) "Category"
+    [10]=>
+    string(11) "Description"
+    [11]=>
+    string(8) "Priority"
+  }
+  [1]=>
+  array(12) {
+    [0]=>
+    string(41) "Inservice on new resource: "CPNP Toolkit""
+    [1]=>
+    string(10) "2004-11-08"
+    [2]=>
+    string(8) "10:30 AM"
+    [3]=>
+    string(10) "2004-11-08"
+    [4]=>
+    string(8) "11:30 AM"
+    [5]=>
+    string(5) "FALSE"
+    [6]=>
+    string(5) "FALSE"
+    [7]=>
+    string(0) ""
+    [8]=>
+    string(0) ""
+    [9]=>
+    string(8) "Training"
+    [10]=>
+    string(1109) "CPN Program ... 
+Inservice on new resource: "CPNP Toolkit"
+
+<b>Registration Deadline:  October 27, 2004, noon</b>
+
+<a href="F041108A-Eval.pdf" target="_blank">
+<img src="acrobat.gif" border="0"></a>  <a href="F041108A-Eval.pdf" target="_blank">  Session Evaluation - Eligibility for Prize!</a>
+
+<a href="F041108A-DI.pdf" target="_blank">
+<img src="acrobat.gif" border="0"></a>  <a href="F041108A-DI.pdf" target="_blank">  Dial In Numbers for Sites Registered</a>
+
+<a href="F041108A.pdf" target="_blank">
+<img src="acrobat.gif" border="0"></a>  <a href="F041108A.pdf" target="_blank">  Poster and Registration Form</a>
+
+Facilitator:  Manager 
+
+preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb  "CPNP Toolkit".  postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb .
+
+postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb 
+
+Come check out the new resource!"
+    [11]=>
+    string(6) "Normal"
+  }
+}
diff --git a/framework/File_CSV/tests/bug_4025.csv b/framework/File_CSV/tests/bug_4025.csv
new file mode 100644 (file)
index 0000000..cd65cc0
--- /dev/null
@@ -0,0 +1,3 @@
+"Betreff","Beginnt am","Beginnt um","Endet am","Endet um","Ganztägiges Ereignis","Erinnerung Ein/Aus","Erinnerung am","Erinnerung um","Besprechungsplanung","Erforderliche Teilnehmer","Optionale Teilnehmer","Besprechungsressourcen","Abrechnungsinformationen","Beschreibung","Kategorien","Ort","Priorität","Privat","Reisekilometer","Vertraulichkeit","Zeitspanne zeigen als"
+"Burger Download Session","2.5.2006","11:50:00","2.5.2006","13:00:00","Aus","Ein","2.5.2006","11:35:00","Haas, Jörg","Kuhl, Oliver",,,,"
+",,"Burger Upload Station (Burger King)","Normal","Aus",,"Normal","1"
diff --git a/framework/File_CSV/tests/bug_4025.phpt b/framework/File_CSV/tests/bug_4025.phpt
new file mode 100644 (file)
index 0000000..edde443
--- /dev/null
@@ -0,0 +1,131 @@
+--TEST--
+File_CSV: test for Bug #4025
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/bug_4025.csv';
+
+// Explicit conf since we can't detect these settings. Might be able
+// to improve auto-detection, but it definitely should work with the
+// settings specified explicitly.
+// var_dump(File_CSV::discoverFormat($file));
+$conf['crlf'] = "\r\n";
+$conf['sep'] = ',';
+$conf['fields'] = 22;
+$conf['quote'] = '"';
+
+$csv = array();
+while ($row = File_CSV::read($file, $conf)) {
+    if (is_a($row, 'PEAR_Error')) {
+        var_dump($row);
+        return;
+    }
+    $csv[] = $row;
+}
+var_dump($csv);
+$warnings = File_CSV::warning();
+if (count($warnings)) {
+    var_dump($warnings);
+}
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(22) {
+    [0]=>
+    string(7) "Betreff"
+    [1]=>
+    string(10) "Beginnt am"
+    [2]=>
+    string(10) "Beginnt um"
+    [3]=>
+    string(8) "Endet am"
+    [4]=>
+    string(8) "Endet um"
+    [5]=>
+    string(20) "Ganztägiges Ereignis"
+    [6]=>
+    string(18) "Erinnerung Ein/Aus"
+    [7]=>
+    string(13) "Erinnerung am"
+    [8]=>
+    string(13) "Erinnerung um"
+    [9]=>
+    string(19) "Besprechungsplanung"
+    [10]=>
+    string(24) "Erforderliche Teilnehmer"
+    [11]=>
+    string(20) "Optionale Teilnehmer"
+    [12]=>
+    string(22) "Besprechungsressourcen"
+    [13]=>
+    string(24) "Abrechnungsinformationen"
+    [14]=>
+    string(12) "Beschreibung"
+    [15]=>
+    string(10) "Kategorien"
+    [16]=>
+    string(3) "Ort"
+    [17]=>
+    string(9) "Priorität"
+    [18]=>
+    string(6) "Privat"
+    [19]=>
+    string(14) "Reisekilometer"
+    [20]=>
+    string(15) "Vertraulichkeit"
+    [21]=>
+    string(21) "Zeitspanne zeigen als"
+  }
+  [1]=>
+  array(22) {
+    [0]=>
+    string(23) "Burger Download Session"
+    [1]=>
+    string(8) "2.5.2006"
+    [2]=>
+    string(8) "11:50:00"
+    [3]=>
+    string(8) "2.5.2006"
+    [4]=>
+    string(8) "13:00:00"
+    [5]=>
+    string(3) "Aus"
+    [6]=>
+    string(3) "Ein"
+    [7]=>
+    string(8) "2.5.2006"
+    [8]=>
+    string(8) "11:35:00"
+    [9]=>
+    string(10) "Haas, Jörg"
+    [10]=>
+    string(12) "Kuhl, Oliver"
+    [11]=>
+    string(0) ""
+    [12]=>
+    string(0) ""
+    [13]=>
+    string(0) ""
+    [14]=>
+    string(1) "
+"
+    [15]=>
+    string(0) ""
+    [16]=>
+    string(35) "Burger Upload Station (Burger King)"
+    [17]=>
+    string(6) "Normal"
+    [18]=>
+    string(3) "Aus"
+    [19]=>
+    string(0) ""
+    [20]=>
+    string(6) "Normal"
+    [21]=>
+    string(1) "1"
+  }
+}
diff --git a/framework/File_CSV/tests/bug_6311.csv b/framework/File_CSV/tests/bug_6311.csv
new file mode 100644 (file)
index 0000000..ef4f25f
--- /dev/null
@@ -0,0 +1,5 @@
+"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page"
+"","John","","Smith","","International Inc","","","",,,"","","","","",,,"","","","","",,,"","","","","","(123) 555-1111","(123) 555-2222","","","","","","","","","(123) 555-3333","","","","","","","","","0/0/00","",,"0/0/00",,"Programming",,,"john@example.com","SMTP","John Smith (john@example.com)",,,,,,,"Unspecified","",,,"J.S.","","","","","",,"PHP
+Perl
+Python
+","","",,"Normal","False","",,"Normal","","","","","",""
diff --git a/framework/File_CSV/tests/bug_6311.phpt b/framework/File_CSV/tests/bug_6311.phpt
new file mode 100644 (file)
index 0000000..b2f4428
--- /dev/null
@@ -0,0 +1,413 @@
+--TEST--
+File_CSV: test for Bug #6311
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/bug_6311.csv';
+
+// Explicit conf since we can't detect these settings. Might be able
+// to improve auto-detection, but it definitely should work with the
+// settings specified explicitly.
+// var_dump(File_CSV::discoverFormat($file));
+$conf['crlf'] = "\n";
+$conf['sep'] = ',';
+$conf['fields'] = 92;
+$conf['quote'] = '"';
+
+$csv = array();
+while ($row = File_CSV::read($file, $conf)) {
+    if (is_a($row, 'PEAR_Error')) {
+        var_dump($row);
+        exit;
+    }
+    $csv[] = $row;
+}
+var_dump($csv);
+$warnings = File_CSV::warning();
+if (count($warnings)) {
+    var_dump($warnings);
+}
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(92) {
+    [0]=>
+    string(5) "Title"
+    [1]=>
+    string(10) "First Name"
+    [2]=>
+    string(11) "Middle Name"
+    [3]=>
+    string(9) "Last Name"
+    [4]=>
+    string(6) "Suffix"
+    [5]=>
+    string(7) "Company"
+    [6]=>
+    string(10) "Department"
+    [7]=>
+    string(9) "Job Title"
+    [8]=>
+    string(15) "Business Street"
+    [9]=>
+    string(17) "Business Street 2"
+    [10]=>
+    string(17) "Business Street 3"
+    [11]=>
+    string(13) "Business City"
+    [12]=>
+    string(14) "Business State"
+    [13]=>
+    string(20) "Business Postal Code"
+    [14]=>
+    string(23) "Business Country/Region"
+    [15]=>
+    string(11) "Home Street"
+    [16]=>
+    string(13) "Home Street 2"
+    [17]=>
+    string(13) "Home Street 3"
+    [18]=>
+    string(9) "Home City"
+    [19]=>
+    string(10) "Home State"
+    [20]=>
+    string(16) "Home Postal Code"
+    [21]=>
+    string(19) "Home Country/Region"
+    [22]=>
+    string(12) "Other Street"
+    [23]=>
+    string(14) "Other Street 2"
+    [24]=>
+    string(14) "Other Street 3"
+    [25]=>
+    string(10) "Other City"
+    [26]=>
+    string(11) "Other State"
+    [27]=>
+    string(17) "Other Postal Code"
+    [28]=>
+    string(20) "Other Country/Region"
+    [29]=>
+    string(17) "Assistant's Phone"
+    [30]=>
+    string(12) "Business Fax"
+    [31]=>
+    string(14) "Business Phone"
+    [32]=>
+    string(16) "Business Phone 2"
+    [33]=>
+    string(8) "Callback"
+    [34]=>
+    string(9) "Car Phone"
+    [35]=>
+    string(18) "Company Main Phone"
+    [36]=>
+    string(8) "Home Fax"
+    [37]=>
+    string(10) "Home Phone"
+    [38]=>
+    string(12) "Home Phone 2"
+    [39]=>
+    string(4) "ISDN"
+    [40]=>
+    string(12) "Mobile Phone"
+    [41]=>
+    string(9) "Other Fax"
+    [42]=>
+    string(11) "Other Phone"
+    [43]=>
+    string(5) "Pager"
+    [44]=>
+    string(13) "Primary Phone"
+    [45]=>
+    string(11) "Radio Phone"
+    [46]=>
+    string(13) "TTY/TDD Phone"
+    [47]=>
+    string(5) "Telex"
+    [48]=>
+    string(7) "Account"
+    [49]=>
+    string(11) "Anniversary"
+    [50]=>
+    string(16) "Assistant's Name"
+    [51]=>
+    string(19) "Billing Information"
+    [52]=>
+    string(8) "Birthday"
+    [53]=>
+    string(23) "Business Address PO Box"
+    [54]=>
+    string(10) "Categories"
+    [55]=>
+    string(8) "Children"
+    [56]=>
+    string(16) "Directory Server"
+    [57]=>
+    string(14) "E-mail Address"
+    [58]=>
+    string(11) "E-mail Type"
+    [59]=>
+    string(19) "E-mail Display Name"
+    [60]=>
+    string(16) "E-mail 2 Address"
+    [61]=>
+    string(13) "E-mail 2 Type"
+    [62]=>
+    string(21) "E-mail 2 Display Name"
+    [63]=>
+    string(16) "E-mail 3 Address"
+    [64]=>
+    string(13) "E-mail 3 Type"
+    [65]=>
+    string(21) "E-mail 3 Display Name"
+    [66]=>
+    string(6) "Gender"
+    [67]=>
+    string(20) "Government ID Number"
+    [68]=>
+    string(5) "Hobby"
+    [69]=>
+    string(19) "Home Address PO Box"
+    [70]=>
+    string(8) "Initials"
+    [71]=>
+    string(18) "Internet Free Busy"
+    [72]=>
+    string(8) "Keywords"
+    [73]=>
+    string(8) "Language"
+    [74]=>
+    string(8) "Location"
+    [75]=>
+    string(14) "Manager's Name"
+    [76]=>
+    string(7) "Mileage"
+    [77]=>
+    string(5) "Notes"
+    [78]=>
+    string(15) "Office Location"
+    [79]=>
+    string(24) "Organizational ID Number"
+    [80]=>
+    string(20) "Other Address PO Box"
+    [81]=>
+    string(8) "Priority"
+    [82]=>
+    string(7) "Private"
+    [83]=>
+    string(10) "Profession"
+    [84]=>
+    string(11) "Referred By"
+    [85]=>
+    string(11) "Sensitivity"
+    [86]=>
+    string(6) "Spouse"
+    [87]=>
+    string(6) "User 1"
+    [88]=>
+    string(6) "User 2"
+    [89]=>
+    string(6) "User 3"
+    [90]=>
+    string(6) "User 4"
+    [91]=>
+    string(8) "Web Page"
+  }
+  [1]=>
+  array(92) {
+    [0]=>
+    string(0) ""
+    [1]=>
+    string(4) "John"
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(5) "Smith"
+    [4]=>
+    string(0) ""
+    [5]=>
+    string(17) "International Inc"
+    [6]=>
+    string(0) ""
+    [7]=>
+    string(0) ""
+    [8]=>
+    string(0) ""
+    [9]=>
+    string(0) ""
+    [10]=>
+    string(0) ""
+    [11]=>
+    string(0) ""
+    [12]=>
+    string(0) ""
+    [13]=>
+    string(0) ""
+    [14]=>
+    string(0) ""
+    [15]=>
+    string(0) ""
+    [16]=>
+    string(0) ""
+    [17]=>
+    string(0) ""
+    [18]=>
+    string(0) ""
+    [19]=>
+    string(0) ""
+    [20]=>
+    string(0) ""
+    [21]=>
+    string(0) ""
+    [22]=>
+    string(0) ""
+    [23]=>
+    string(0) ""
+    [24]=>
+    string(0) ""
+    [25]=>
+    string(0) ""
+    [26]=>
+    string(0) ""
+    [27]=>
+    string(0) ""
+    [28]=>
+    string(0) ""
+    [29]=>
+    string(0) ""
+    [30]=>
+    string(14) "(123) 555-1111"
+    [31]=>
+    string(14) "(123) 555-2222"
+    [32]=>
+    string(0) ""
+    [33]=>
+    string(0) ""
+    [34]=>
+    string(0) ""
+    [35]=>
+    string(0) ""
+    [36]=>
+    string(0) ""
+    [37]=>
+    string(0) ""
+    [38]=>
+    string(0) ""
+    [39]=>
+    string(0) ""
+    [40]=>
+    string(14) "(123) 555-3333"
+    [41]=>
+    string(0) ""
+    [42]=>
+    string(0) ""
+    [43]=>
+    string(0) ""
+    [44]=>
+    string(0) ""
+    [45]=>
+    string(0) ""
+    [46]=>
+    string(0) ""
+    [47]=>
+    string(0) ""
+    [48]=>
+    string(0) ""
+    [49]=>
+    string(6) "0/0/00"
+    [50]=>
+    string(0) ""
+    [51]=>
+    string(0) ""
+    [52]=>
+    string(6) "0/0/00"
+    [53]=>
+    string(0) ""
+    [54]=>
+    string(11) "Programming"
+    [55]=>
+    string(0) ""
+    [56]=>
+    string(0) ""
+    [57]=>
+    string(16) "john@example.com"
+    [58]=>
+    string(4) "SMTP"
+    [59]=>
+    string(29) "John Smith (john@example.com)"
+    [60]=>
+    string(0) ""
+    [61]=>
+    string(0) ""
+    [62]=>
+    string(0) ""
+    [63]=>
+    string(0) ""
+    [64]=>
+    string(0) ""
+    [65]=>
+    string(0) ""
+    [66]=>
+    string(11) "Unspecified"
+    [67]=>
+    string(0) ""
+    [68]=>
+    string(0) ""
+    [69]=>
+    string(0) ""
+    [70]=>
+    string(4) "J.S."
+    [71]=>
+    string(0) ""
+    [72]=>
+    string(0) ""
+    [73]=>
+    string(0) ""
+    [74]=>
+    string(0) ""
+    [75]=>
+    string(0) ""
+    [76]=>
+    string(0) ""
+    [77]=>
+    string(16) "PHP
+Perl
+Python
+"
+    [78]=>
+    string(0) ""
+    [79]=>
+    string(0) ""
+    [80]=>
+    string(0) ""
+    [81]=>
+    string(6) "Normal"
+    [82]=>
+    string(5) "False"
+    [83]=>
+    string(0) ""
+    [84]=>
+    string(0) ""
+    [85]=>
+    string(6) "Normal"
+    [86]=>
+    string(0) ""
+    [87]=>
+    string(0) ""
+    [88]=>
+    string(0) ""
+    [89]=>
+    string(0) ""
+    [90]=>
+    string(0) ""
+    [91]=>
+    string(0) ""
+  }
+}
diff --git a/framework/File_CSV/tests/bug_6370.csv b/framework/File_CSV/tests/bug_6370.csv
new file mode 100644 (file)
index 0000000..9a96902
--- /dev/null
@@ -0,0 +1,3 @@
+"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page"
+"","","","","","","","","Big Tower'"", 1"" Floor
+123 Main Street",,,"","","","","",,,"","","","","",,,"","","","","","","","","","","","","","","","","","","","","","","","","0/0/00","",,"0/0/00",,,,,"","","",,,,,,,"Unspecified","",,,"","","","","","",,,"","",,"Normal","False","",,"Normal","","","","","",""
diff --git a/framework/File_CSV/tests/bug_6370.phpt b/framework/File_CSV/tests/bug_6370.phpt
new file mode 100644 (file)
index 0000000..a322e27
--- /dev/null
@@ -0,0 +1,411 @@
+--TEST--
+File_CSV: test for Bug #6370
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/bug_6370.csv';
+
+// Explicit conf since we can't detect these settings. Might be able
+// to improve auto-detection, but it definitely should work with the
+// settings specified explicitly.
+// var_dump(File_CSV::discoverFormat($file));
+$conf['crlf'] = "\n";
+$conf['sep'] = ',';
+$conf['fields'] = 92;
+$conf['quote'] = '"';
+
+$csv = array();
+while ($row = File_CSV::read($file, $conf)) {
+    if (is_a($row, 'PEAR_Error')) {
+        var_dump($row);
+        exit;
+    }
+    $csv[] = $row;
+}
+var_dump($csv);
+$warnings = File_CSV::warning();
+if (count($warnings)) {
+    var_dump($warnings);
+}
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(92) {
+    [0]=>
+    string(5) "Title"
+    [1]=>
+    string(10) "First Name"
+    [2]=>
+    string(11) "Middle Name"
+    [3]=>
+    string(9) "Last Name"
+    [4]=>
+    string(6) "Suffix"
+    [5]=>
+    string(7) "Company"
+    [6]=>
+    string(10) "Department"
+    [7]=>
+    string(9) "Job Title"
+    [8]=>
+    string(15) "Business Street"
+    [9]=>
+    string(17) "Business Street 2"
+    [10]=>
+    string(17) "Business Street 3"
+    [11]=>
+    string(13) "Business City"
+    [12]=>
+    string(14) "Business State"
+    [13]=>
+    string(20) "Business Postal Code"
+    [14]=>
+    string(23) "Business Country/Region"
+    [15]=>
+    string(11) "Home Street"
+    [16]=>
+    string(13) "Home Street 2"
+    [17]=>
+    string(13) "Home Street 3"
+    [18]=>
+    string(9) "Home City"
+    [19]=>
+    string(10) "Home State"
+    [20]=>
+    string(16) "Home Postal Code"
+    [21]=>
+    string(19) "Home Country/Region"
+    [22]=>
+    string(12) "Other Street"
+    [23]=>
+    string(14) "Other Street 2"
+    [24]=>
+    string(14) "Other Street 3"
+    [25]=>
+    string(10) "Other City"
+    [26]=>
+    string(11) "Other State"
+    [27]=>
+    string(17) "Other Postal Code"
+    [28]=>
+    string(20) "Other Country/Region"
+    [29]=>
+    string(17) "Assistant's Phone"
+    [30]=>
+    string(12) "Business Fax"
+    [31]=>
+    string(14) "Business Phone"
+    [32]=>
+    string(16) "Business Phone 2"
+    [33]=>
+    string(8) "Callback"
+    [34]=>
+    string(9) "Car Phone"
+    [35]=>
+    string(18) "Company Main Phone"
+    [36]=>
+    string(8) "Home Fax"
+    [37]=>
+    string(10) "Home Phone"
+    [38]=>
+    string(12) "Home Phone 2"
+    [39]=>
+    string(4) "ISDN"
+    [40]=>
+    string(12) "Mobile Phone"
+    [41]=>
+    string(9) "Other Fax"
+    [42]=>
+    string(11) "Other Phone"
+    [43]=>
+    string(5) "Pager"
+    [44]=>
+    string(13) "Primary Phone"
+    [45]=>
+    string(11) "Radio Phone"
+    [46]=>
+    string(13) "TTY/TDD Phone"
+    [47]=>
+    string(5) "Telex"
+    [48]=>
+    string(7) "Account"
+    [49]=>
+    string(11) "Anniversary"
+    [50]=>
+    string(16) "Assistant's Name"
+    [51]=>
+    string(19) "Billing Information"
+    [52]=>
+    string(8) "Birthday"
+    [53]=>
+    string(23) "Business Address PO Box"
+    [54]=>
+    string(10) "Categories"
+    [55]=>
+    string(8) "Children"
+    [56]=>
+    string(16) "Directory Server"
+    [57]=>
+    string(14) "E-mail Address"
+    [58]=>
+    string(11) "E-mail Type"
+    [59]=>
+    string(19) "E-mail Display Name"
+    [60]=>
+    string(16) "E-mail 2 Address"
+    [61]=>
+    string(13) "E-mail 2 Type"
+    [62]=>
+    string(21) "E-mail 2 Display Name"
+    [63]=>
+    string(16) "E-mail 3 Address"
+    [64]=>
+    string(13) "E-mail 3 Type"
+    [65]=>
+    string(21) "E-mail 3 Display Name"
+    [66]=>
+    string(6) "Gender"
+    [67]=>
+    string(20) "Government ID Number"
+    [68]=>
+    string(5) "Hobby"
+    [69]=>
+    string(19) "Home Address PO Box"
+    [70]=>
+    string(8) "Initials"
+    [71]=>
+    string(18) "Internet Free Busy"
+    [72]=>
+    string(8) "Keywords"
+    [73]=>
+    string(8) "Language"
+    [74]=>
+    string(8) "Location"
+    [75]=>
+    string(14) "Manager's Name"
+    [76]=>
+    string(7) "Mileage"
+    [77]=>
+    string(5) "Notes"
+    [78]=>
+    string(15) "Office Location"
+    [79]=>
+    string(24) "Organizational ID Number"
+    [80]=>
+    string(20) "Other Address PO Box"
+    [81]=>
+    string(8) "Priority"
+    [82]=>
+    string(7) "Private"
+    [83]=>
+    string(10) "Profession"
+    [84]=>
+    string(11) "Referred By"
+    [85]=>
+    string(11) "Sensitivity"
+    [86]=>
+    string(6) "Spouse"
+    [87]=>
+    string(6) "User 1"
+    [88]=>
+    string(6) "User 2"
+    [89]=>
+    string(6) "User 3"
+    [90]=>
+    string(6) "User 4"
+    [91]=>
+    string(8) "Web Page"
+  }
+  [1]=>
+  array(92) {
+    [0]=>
+    string(0) ""
+    [1]=>
+    string(0) ""
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+    [4]=>
+    string(0) ""
+    [5]=>
+    string(0) ""
+    [6]=>
+    string(0) ""
+    [7]=>
+    string(0) ""
+    [8]=>
+    string(37) "Big Tower'", 1" Floor
+123 Main Street"
+    [9]=>
+    string(0) ""
+    [10]=>
+    string(0) ""
+    [11]=>
+    string(0) ""
+    [12]=>
+    string(0) ""
+    [13]=>
+    string(0) ""
+    [14]=>
+    string(0) ""
+    [15]=>
+    string(0) ""
+    [16]=>
+    string(0) ""
+    [17]=>
+    string(0) ""
+    [18]=>
+    string(0) ""
+    [19]=>
+    string(0) ""
+    [20]=>
+    string(0) ""
+    [21]=>
+    string(0) ""
+    [22]=>
+    string(0) ""
+    [23]=>
+    string(0) ""
+    [24]=>
+    string(0) ""
+    [25]=>
+    string(0) ""
+    [26]=>
+    string(0) ""
+    [27]=>
+    string(0) ""
+    [28]=>
+    string(0) ""
+    [29]=>
+    string(0) ""
+    [30]=>
+    string(0) ""
+    [31]=>
+    string(0) ""
+    [32]=>
+    string(0) ""
+    [33]=>
+    string(0) ""
+    [34]=>
+    string(0) ""
+    [35]=>
+    string(0) ""
+    [36]=>
+    string(0) ""
+    [37]=>
+    string(0) ""
+    [38]=>
+    string(0) ""
+    [39]=>
+    string(0) ""
+    [40]=>
+    string(0) ""
+    [41]=>
+    string(0) ""
+    [42]=>
+    string(0) ""
+    [43]=>
+    string(0) ""
+    [44]=>
+    string(0) ""
+    [45]=>
+    string(0) ""
+    [46]=>
+    string(0) ""
+    [47]=>
+    string(0) ""
+    [48]=>
+    string(0) ""
+    [49]=>
+    string(6) "0/0/00"
+    [50]=>
+    string(0) ""
+    [51]=>
+    string(0) ""
+    [52]=>
+    string(6) "0/0/00"
+    [53]=>
+    string(0) ""
+    [54]=>
+    string(0) ""
+    [55]=>
+    string(0) ""
+    [56]=>
+    string(0) ""
+    [57]=>
+    string(0) ""
+    [58]=>
+    string(0) ""
+    [59]=>
+    string(0) ""
+    [60]=>
+    string(0) ""
+    [61]=>
+    string(0) ""
+    [62]=>
+    string(0) ""
+    [63]=>
+    string(0) ""
+    [64]=>
+    string(0) ""
+    [65]=>
+    string(0) ""
+    [66]=>
+    string(11) "Unspecified"
+    [67]=>
+    string(0) ""
+    [68]=>
+    string(0) ""
+    [69]=>
+    string(0) ""
+    [70]=>
+    string(0) ""
+    [71]=>
+    string(0) ""
+    [72]=>
+    string(0) ""
+    [73]=>
+    string(0) ""
+    [74]=>
+    string(0) ""
+    [75]=>
+    string(0) ""
+    [76]=>
+    string(0) ""
+    [77]=>
+    string(1) ""
+    [78]=>
+    string(0) ""
+    [79]=>
+    string(0) ""
+    [80]=>
+    string(0) ""
+    [81]=>
+    string(6) "Normal"
+    [82]=>
+    string(5) "False"
+    [83]=>
+    string(0) ""
+    [84]=>
+    string(0) ""
+    [85]=>
+    string(6) "Normal"
+    [86]=>
+    string(0) ""
+    [87]=>
+    string(0) ""
+    [88]=>
+    string(0) ""
+    [89]=>
+    string(0) ""
+    [90]=>
+    string(0) ""
+    [91]=>
+    string(0) ""
+  }
+}
diff --git a/framework/File_CSV/tests/bug_6372.csv b/framework/File_CSV/tests/bug_6372.csv
new file mode 100644 (file)
index 0000000..ec31094
--- /dev/null
@@ -0,0 +1,4 @@
+"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page"
+"","","","","","","","","123, 12th Floor,
+Main Street",,,"","","","","",,,"","","","","",,,"","","","","","","","","","","","","","","","","","","","","","","","","0/0/00","",,"0/0/00",,"",,,"","","","","","",,,,"Unspecified","",,,"","","","","","",,"
+","","",,"Normal","False","",,"Normal","","","","","",""
diff --git a/framework/File_CSV/tests/bug_6372.phpt b/framework/File_CSV/tests/bug_6372.phpt
new file mode 100644 (file)
index 0000000..5891647
--- /dev/null
@@ -0,0 +1,412 @@
+--TEST--
+File_CSV: test for Bug #6372
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+$file = dirname(__FILE__) . '/bug_6372.csv';
+
+// Explicit conf since we can't detect these settings. Might be able
+// to improve auto-detection, but it definitely should work with the
+// settings specified explicitly.
+// var_dump(File_CSV::discoverFormat($file));
+$conf['crlf'] = "\n";
+$conf['sep'] = ',';
+$conf['fields'] = 92;
+$conf['quote'] = '"';
+
+$csv = array();
+while ($row = File_CSV::read($file, $conf)) {
+    if (is_a($row, 'PEAR_Error')) {
+        var_dump($row);
+        exit;
+    }
+    $csv[] = $row;
+}
+var_dump($csv);
+$warnings = File_CSV::warning();
+if (count($warnings)) {
+    var_dump($warnings);
+}
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(92) {
+    [0]=>
+    string(5) "Title"
+    [1]=>
+    string(10) "First Name"
+    [2]=>
+    string(11) "Middle Name"
+    [3]=>
+    string(9) "Last Name"
+    [4]=>
+    string(6) "Suffix"
+    [5]=>
+    string(7) "Company"
+    [6]=>
+    string(10) "Department"
+    [7]=>
+    string(9) "Job Title"
+    [8]=>
+    string(15) "Business Street"
+    [9]=>
+    string(17) "Business Street 2"
+    [10]=>
+    string(17) "Business Street 3"
+    [11]=>
+    string(13) "Business City"
+    [12]=>
+    string(14) "Business State"
+    [13]=>
+    string(20) "Business Postal Code"
+    [14]=>
+    string(23) "Business Country/Region"
+    [15]=>
+    string(11) "Home Street"
+    [16]=>
+    string(13) "Home Street 2"
+    [17]=>
+    string(13) "Home Street 3"
+    [18]=>
+    string(9) "Home City"
+    [19]=>
+    string(10) "Home State"
+    [20]=>
+    string(16) "Home Postal Code"
+    [21]=>
+    string(19) "Home Country/Region"
+    [22]=>
+    string(12) "Other Street"
+    [23]=>
+    string(14) "Other Street 2"
+    [24]=>
+    string(14) "Other Street 3"
+    [25]=>
+    string(10) "Other City"
+    [26]=>
+    string(11) "Other State"
+    [27]=>
+    string(17) "Other Postal Code"
+    [28]=>
+    string(20) "Other Country/Region"
+    [29]=>
+    string(17) "Assistant's Phone"
+    [30]=>
+    string(12) "Business Fax"
+    [31]=>
+    string(14) "Business Phone"
+    [32]=>
+    string(16) "Business Phone 2"
+    [33]=>
+    string(8) "Callback"
+    [34]=>
+    string(9) "Car Phone"
+    [35]=>
+    string(18) "Company Main Phone"
+    [36]=>
+    string(8) "Home Fax"
+    [37]=>
+    string(10) "Home Phone"
+    [38]=>
+    string(12) "Home Phone 2"
+    [39]=>
+    string(4) "ISDN"
+    [40]=>
+    string(12) "Mobile Phone"
+    [41]=>
+    string(9) "Other Fax"
+    [42]=>
+    string(11) "Other Phone"
+    [43]=>
+    string(5) "Pager"
+    [44]=>
+    string(13) "Primary Phone"
+    [45]=>
+    string(11) "Radio Phone"
+    [46]=>
+    string(13) "TTY/TDD Phone"
+    [47]=>
+    string(5) "Telex"
+    [48]=>
+    string(7) "Account"
+    [49]=>
+    string(11) "Anniversary"
+    [50]=>
+    string(16) "Assistant's Name"
+    [51]=>
+    string(19) "Billing Information"
+    [52]=>
+    string(8) "Birthday"
+    [53]=>
+    string(23) "Business Address PO Box"
+    [54]=>
+    string(10) "Categories"
+    [55]=>
+    string(8) "Children"
+    [56]=>
+    string(16) "Directory Server"
+    [57]=>
+    string(14) "E-mail Address"
+    [58]=>
+    string(11) "E-mail Type"
+    [59]=>
+    string(19) "E-mail Display Name"
+    [60]=>
+    string(16) "E-mail 2 Address"
+    [61]=>
+    string(13) "E-mail 2 Type"
+    [62]=>
+    string(21) "E-mail 2 Display Name"
+    [63]=>
+    string(16) "E-mail 3 Address"
+    [64]=>
+    string(13) "E-mail 3 Type"
+    [65]=>
+    string(21) "E-mail 3 Display Name"
+    [66]=>
+    string(6) "Gender"
+    [67]=>
+    string(20) "Government ID Number"
+    [68]=>
+    string(5) "Hobby"
+    [69]=>
+    string(19) "Home Address PO Box"
+    [70]=>
+    string(8) "Initials"
+    [71]=>
+    string(18) "Internet Free Busy"
+    [72]=>
+    string(8) "Keywords"
+    [73]=>
+    string(8) "Language"
+    [74]=>
+    string(8) "Location"
+    [75]=>
+    string(14) "Manager's Name"
+    [76]=>
+    string(7) "Mileage"
+    [77]=>
+    string(5) "Notes"
+    [78]=>
+    string(15) "Office Location"
+    [79]=>
+    string(24) "Organizational ID Number"
+    [80]=>
+    string(20) "Other Address PO Box"
+    [81]=>
+    string(8) "Priority"
+    [82]=>
+    string(7) "Private"
+    [83]=>
+    string(10) "Profession"
+    [84]=>
+    string(11) "Referred By"
+    [85]=>
+    string(11) "Sensitivity"
+    [86]=>
+    string(6) "Spouse"
+    [87]=>
+    string(6) "User 1"
+    [88]=>
+    string(6) "User 2"
+    [89]=>
+    string(6) "User 3"
+    [90]=>
+    string(6) "User 4"
+    [91]=>
+    string(8) "Web Page"
+  }
+  [1]=>
+  array(92) {
+    [0]=>
+    string(0) ""
+    [1]=>
+    string(0) ""
+    [2]=>
+    string(0) ""
+    [3]=>
+    string(0) ""
+    [4]=>
+    string(0) ""
+    [5]=>
+    string(0) ""
+    [6]=>
+    string(0) ""
+    [7]=>
+    string(0) ""
+    [8]=>
+    string(28) "123, 12th Floor,
+Main Street"
+    [9]=>
+    string(0) ""
+    [10]=>
+    string(0) ""
+    [11]=>
+    string(0) ""
+    [12]=>
+    string(0) ""
+    [13]=>
+    string(0) ""
+    [14]=>
+    string(0) ""
+    [15]=>
+    string(0) ""
+    [16]=>
+    string(0) ""
+    [17]=>
+    string(0) ""
+    [18]=>
+    string(0) ""
+    [19]=>
+    string(0) ""
+    [20]=>
+    string(0) ""
+    [21]=>
+    string(0) ""
+    [22]=>
+    string(0) ""
+    [23]=>
+    string(0) ""
+    [24]=>
+    string(0) ""
+    [25]=>
+    string(0) ""
+    [26]=>
+    string(0) ""
+    [27]=>
+    string(0) ""
+    [28]=>
+    string(0) ""
+    [29]=>
+    string(0) ""
+    [30]=>
+    string(0) ""
+    [31]=>
+    string(0) ""
+    [32]=>
+    string(0) ""
+    [33]=>
+    string(0) ""
+    [34]=>
+    string(0) ""
+    [35]=>
+    string(0) ""
+    [36]=>
+    string(0) ""
+    [37]=>
+    string(0) ""
+    [38]=>
+    string(0) ""
+    [39]=>
+    string(0) ""
+    [40]=>
+    string(0) ""
+    [41]=>
+    string(0) ""
+    [42]=>
+    string(0) ""
+    [43]=>
+    string(0) ""
+    [44]=>
+    string(0) ""
+    [45]=>
+    string(0) ""
+    [46]=>
+    string(0) ""
+    [47]=>
+    string(0) ""
+    [48]=>
+    string(0) ""
+    [49]=>
+    string(6) "0/0/00"
+    [50]=>
+    string(0) ""
+    [51]=>
+    string(0) ""
+    [52]=>
+    string(6) "0/0/00"
+    [53]=>
+    string(0) ""
+    [54]=>
+    string(0) ""
+    [55]=>
+    string(0) ""
+    [56]=>
+    string(0) ""
+    [57]=>
+    string(0) ""
+    [58]=>
+    string(0) ""
+    [59]=>
+    string(0) ""
+    [60]=>
+    string(0) ""
+    [61]=>
+    string(0) ""
+    [62]=>
+    string(0) ""
+    [63]=>
+    string(0) ""
+    [64]=>
+    string(0) ""
+    [65]=>
+    string(0) ""
+    [66]=>
+    string(11) "Unspecified"
+    [67]=>
+    string(0) ""
+    [68]=>
+    string(0) ""
+    [69]=>
+    string(0) ""
+    [70]=>
+    string(0) ""
+    [71]=>
+    string(0) ""
+    [72]=>
+    string(0) ""
+    [73]=>
+    string(0) ""
+    [74]=>
+    string(0) ""
+    [75]=>
+    string(0) ""
+    [76]=>
+    string(0) ""
+    [77]=>
+    string(1) "
+"
+    [78]=>
+    string(0) ""
+    [79]=>
+    string(0) ""
+    [80]=>
+    string(0) ""
+    [81]=>
+    string(6) "Normal"
+    [82]=>
+    string(5) "False"
+    [83]=>
+    string(0) ""
+    [84]=>
+    string(0) ""
+    [85]=>
+    string(6) "Normal"
+    [86]=>
+    string(0) ""
+    [87]=>
+    string(0) ""
+    [88]=>
+    string(0) ""
+    [89]=>
+    string(0) ""
+    [90]=>
+    string(0) ""
+    [91]=>
+    string(0) ""
+  }
+}
diff --git a/framework/File_CSV/tests/columns.phpt b/framework/File_CSV/tests/columns.phpt
new file mode 100644 (file)
index 0000000..c88b574
--- /dev/null
@@ -0,0 +1,98 @@
+--TEST--
+File_CSV: column count tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/common.php';
+test_csv('columns1', 'columns2');
+
+?>
+--EXPECT--
+array(4) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(0) ""
+  }
+  [2]=>
+  array(3) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(5) "seven"
+    [2]=>
+    string(5) "eight"
+  }
+  [3]=>
+  array(3) {
+    [0]=>
+    string(4) "nine"
+    [1]=>
+    string(3) "ten"
+    [2]=>
+    string(6) "eleven"
+  }
+}
+array(2) {
+  [0]=>
+  string(54) "Wrong number of fields in line 2. Expected 3, found 2."
+  [1]=>
+  string(48) "More fields found in line 4 than the expected 3."
+}
+array(4) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(0) ""
+  }
+  [2]=>
+  array(3) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(5) "seven"
+    [2]=>
+    string(5) "eight"
+  }
+  [3]=>
+  array(3) {
+    [0]=>
+    string(4) "nine"
+    [1]=>
+    string(3) "ten"
+    [2]=>
+    string(6) "eleven"
+  }
+}
+array(2) {
+  [0]=>
+  string(54) "Wrong number of fields in line 2. Expected 3, found 2."
+  [1]=>
+  string(48) "More fields found in line 4 than the expected 3."
+}
diff --git a/framework/File_CSV/tests/columns1.csv b/framework/File_CSV/tests/columns1.csv
new file mode 100644 (file)
index 0000000..be92c82
--- /dev/null
@@ -0,0 +1,4 @@
+one,two,three
+four,five
+six,seven,eight
+nine,ten,eleven,twelve
diff --git a/framework/File_CSV/tests/columns2.csv b/framework/File_CSV/tests/columns2.csv
new file mode 100644 (file)
index 0000000..da81f1e
--- /dev/null
@@ -0,0 +1,4 @@
+"one","two","three"
+"four","five"
+"six","seven","eight"
+"nine","ten","eleven","twelve"
diff --git a/framework/File_CSV/tests/common.php b/framework/File_CSV/tests/common.php
new file mode 100644 (file)
index 0000000..7677dd8
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @package File_CSV
+ */
+
+require_once dirname(__FILE__) . '/../CSV.php';
+
+function test_csv()
+{
+    foreach (func_get_args() as $file) {
+        $file = dirname(__FILE__) . '/' . $file . '.csv';
+        $conf = File_CSV::discoverFormat($file);
+        if (is_a($conf, 'PEAR_Error')) {
+            var_dump($conf);
+            return;
+        }
+        $csv = array();
+        while ($row = File_CSV::read($file, $conf)) {
+            if (is_a($row, 'PEAR_Error')) {
+                var_dump($row);
+                return;
+            }
+            $csv[] = $row;
+        }
+        var_dump($csv);
+        $warnings = File_CSV::warning();
+        if (count($warnings)) {
+            var_dump($warnings);
+        }
+    }
+}
diff --git a/framework/File_CSV/tests/linebreak.phpt b/framework/File_CSV/tests/linebreak.phpt
new file mode 100644 (file)
index 0000000..cb91de9
--- /dev/null
@@ -0,0 +1,110 @@
+--TEST--
+File_CSV: linebreak tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/common.php';
+test_csv('simple_cr', 'simple_lf', 'simple_crlf', 'notrailing_lf', 'notrailing_crlf');
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(3) "six"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(3) "six"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(3) "six"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(3) "six"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(4) "five"
+    [2]=>
+    string(3) "six"
+  }
+}
diff --git a/framework/File_CSV/tests/multiline.phpt b/framework/File_CSV/tests/multiline.phpt
new file mode 100644 (file)
index 0000000..fc43c41
--- /dev/null
@@ -0,0 +1,52 @@
+--TEST--
+File_CSV: multiline tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/common.php';
+test_csv('multiline1');
+
+?>
+--EXPECT--
+array(4) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(10) "three
+four"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "five"
+    [1]=>
+    string(9) "six
+seven"
+    [2]=>
+    string(5) "eight"
+  }
+  [2]=>
+  array(3) {
+    [0]=>
+    string(4) "nine"
+    [1]=>
+    string(3) "ten"
+    [2]=>
+    string(14) "eleven 
+twelve"
+  }
+  [3]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(11) "three
+ four"
+  }
+}
diff --git a/framework/File_CSV/tests/multiline1.csv b/framework/File_CSV/tests/multiline1.csv
new file mode 100644 (file)
index 0000000..5e836a7
--- /dev/null
@@ -0,0 +1,8 @@
+"one","two","three
+four"
+"five","six
+seven","eight"
+"nine","ten","eleven 
+twelve"
+"one","two","three
+ four"
diff --git a/framework/File_CSV/tests/notrailing_crlf.csv b/framework/File_CSV/tests/notrailing_crlf.csv
new file mode 100644 (file)
index 0000000..5b60c9c
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three
+four,five,six
diff --git a/framework/File_CSV/tests/notrailing_lf.csv b/framework/File_CSV/tests/notrailing_lf.csv
new file mode 100644 (file)
index 0000000..5b60c9c
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three
+four,five,six
diff --git a/framework/File_CSV/tests/quote1.csv b/framework/File_CSV/tests/quote1.csv
new file mode 100644 (file)
index 0000000..e2fe57d
--- /dev/null
@@ -0,0 +1,2 @@
+"one",two,"three"
+four,"five six",seven
diff --git a/framework/File_CSV/tests/quote2.csv b/framework/File_CSV/tests/quote2.csv
new file mode 100644 (file)
index 0000000..e2fe57d
--- /dev/null
@@ -0,0 +1,2 @@
+"one",two,"three"
+four,"five six",seven
diff --git a/framework/File_CSV/tests/quote3.csv b/framework/File_CSV/tests/quote3.csv
new file mode 100644 (file)
index 0000000..c444091
--- /dev/null
@@ -0,0 +1,3 @@
+"one two","three, four",five
+six,"seven ",eight
+
diff --git a/framework/File_CSV/tests/quote4.csv b/framework/File_CSV/tests/quote4.csv
new file mode 100644 (file)
index 0000000..c444091
--- /dev/null
@@ -0,0 +1,3 @@
+"one two","three, four",five
+six,"seven ",eight
+
diff --git a/framework/File_CSV/tests/quote5.csv b/framework/File_CSV/tests/quote5.csv
new file mode 100644 (file)
index 0000000..0629c42
--- /dev/null
@@ -0,0 +1,2 @@
+"one two","three, four","five"
+"six","seven ","eight"
diff --git a/framework/File_CSV/tests/quotes.phpt b/framework/File_CSV/tests/quotes.phpt
new file mode 100644 (file)
index 0000000..c027d9a
--- /dev/null
@@ -0,0 +1,110 @@
+--TEST--
+File_CSV: quote tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/common.php';
+test_csv('quote1', 'quote2', 'quote3', 'quote4', 'quote5');
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(8) "five six"
+    [2]=>
+    string(5) "seven"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(3) "one"
+    [1]=>
+    string(3) "two"
+    [2]=>
+    string(5) "three"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(4) "four"
+    [1]=>
+    string(8) "five six"
+    [2]=>
+    string(5) "seven"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(7) "one two"
+    [1]=>
+    string(11) "three, four"
+    [2]=>
+    string(4) "five"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(6) "seven "
+    [2]=>
+    string(5) "eight"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(7) "one two"
+    [1]=>
+    string(11) "three, four"
+    [2]=>
+    string(4) "five"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(6) "seven "
+    [2]=>
+    string(5) "eight"
+  }
+}
+array(2) {
+  [0]=>
+  array(3) {
+    [0]=>
+    string(7) "one two"
+    [1]=>
+    string(11) "three, four"
+    [2]=>
+    string(4) "five"
+  }
+  [1]=>
+  array(3) {
+    [0]=>
+    string(3) "six"
+    [1]=>
+    string(6) "seven "
+    [2]=>
+    string(5) "eight"
+  }
+}
diff --git a/framework/File_CSV/tests/simple_cr.csv b/framework/File_CSV/tests/simple_cr.csv
new file mode 100644 (file)
index 0000000..beb72ce
--- /dev/null
@@ -0,0 +1 @@
+one,two,three\rfour,five,six\r
\ No newline at end of file
diff --git a/framework/File_CSV/tests/simple_crlf.csv b/framework/File_CSV/tests/simple_crlf.csv
new file mode 100644 (file)
index 0000000..5b60c9c
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three
+four,five,six
diff --git a/framework/File_CSV/tests/simple_lf.csv b/framework/File_CSV/tests/simple_lf.csv
new file mode 100644 (file)
index 0000000..5b60c9c
--- /dev/null
@@ -0,0 +1,2 @@
+one,two,three
+four,five,six
diff --git a/framework/File_PDF/PDF.php b/framework/File_PDF/PDF.php
new file mode 100644 (file)
index 0000000..12c8855
--- /dev/null
@@ -0,0 +1,3178 @@
+<?php
+/**
+ * The File_PDF class provides a PHP-only implementation of a PDF library. No
+ * external libs or PHP extensions are required.
+ *
+ * Based on the FPDF class by Olivier Plathey (http://www.fpdf.org/).
+ *
+ * $Horde: framework/File_PDF/PDF.php,v 1.65 2009/01/06 17:49:16 jan Exp $
+ *
+ * Copyright 2001-2003 Olivier Plathey <olivier@fpdf.org>
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Olivier Plathey <olivier@fpdf.org>
+ * @author   Marko Djukic <marko@oblo.com>
+ * @author   Jan Schneider <jan@horde.org>
+ * @package  File_PDF
+ * @category Fileformats
+ */
+
+/**
+ * This hack works around Horde bug #4094
+ * (http://bugs.horde.org/ticket/?id=4094)
+ *
+ * Once this package does not need to support PHP < 4.3.10 anymore the
+ * following definiton can be removed and the ugly code can be removed
+ * using
+ *
+ * sed -i -e 's/\' \. FILE_PDF_FLOAT \. \'/F/g' PDF.php
+ */
+if (version_compare(PHP_VERSION, '4.3.10', '>=')) {
+    define('FILE_PDF_FLOAT', 'F');
+} else {
+    define('FILE_PDF_FLOAT', 'f');
+}
+
+class File_PDF {
+
+    /**
+     * Current page number.
+     *
+     * @var integer
+     */
+    var $_page = 0;
+
+    /**
+     * Current object number.
+     *
+     * @var integer
+     */
+    var $_n = 2;
+
+    /**
+     * Array of object offsets.
+     *
+     * @var array
+     */
+    var $_offsets = array();
+
+    /**
+     * Buffer holding in-memory PDF.
+     *
+     * @var string
+     */
+    var $_buffer = '';
+
+    /**
+     * Buffer length, including already flushed content.
+     *
+     * @var integer
+     */
+    var $_buflen = 0;
+
+    /**
+     * Whether the buffer has been flushed already.
+     *
+     * @var boolean
+     */
+    var $_flushed = false;
+
+    /**
+     * Array containing the pages.
+     *
+     * @var array
+     */
+    var $_pages = array();
+
+    /**
+     * Current document state.<pre>
+     *   0 - initial state
+     *   1 - document opened
+     *   2 - page opened
+     *   3 - document closed
+     * </pre>
+     *
+     * @var integer
+     */
+    var $_state = 0;
+
+    /**
+     * Flag indicating if PDF file is to be compressed or not.
+     *
+     * @var boolean
+     */
+    var $_compress;
+
+    /**
+     * The default page orientation.
+     *
+     * @var string
+     */
+    var $_default_orientation;
+
+    /**
+     * The current page orientation.
+     *
+     * @var string
+     */
+    var $_current_orientation;
+
+    /**
+     * Array indicating orientation changes.
+     *
+     * @var array
+     */
+    var $_orientation_changes = array();
+
+    /**
+     * Current width of page format in points.
+     *
+     * @var float
+     */
+    var $fwPt;
+
+    /**
+     * Current height of page format in points.
+     *
+     * @var float
+     */
+    var $fhPt;
+
+    /**
+     * Current width of page format in user units.
+     *
+     * @var float
+     */
+    var $fw;
+
+    /**
+     * Current height of page format in user units.
+     *
+     * @var float
+     */
+    var $fh;
+
+    /**
+     * Current width of page in points.
+     *
+     * @var float
+     */
+    var $wPt;
+
+    /**
+     * Current height of page in points.
+     *
+     * @var float
+     */
+    var $hPt;
+
+    /**
+     * Current width of page in user units
+     *
+     * @var float
+     */
+    var $w;
+
+    /**
+     * Current height of page in user units
+     *
+     * @var float
+     */
+    var $h;
+
+    /**
+     * Scale factor (number of points in user units).
+     *
+     * @var float
+     */
+    var $_scale;
+
+    /**
+     * Left page margin size.
+     *
+     * @var float
+     */
+    var $_left_margin;
+
+    /**
+     * Top page margin size.
+     *
+     * @var float
+     */
+    var $_top_margin;
+
+    /**
+     * Right page margin size.
+     *
+     * @var float
+     */
+    var $_right_margin;
+
+    /**
+     * Break page margin size, the bottom margin which triggers a page break.
+     *
+     * @var float
+     */
+    var $_break_margin;
+
+    /**
+     * Cell margin size.
+     *
+     * @var float
+     */
+    var $_cell_margin;
+
+    /**
+     * The current horizontal position for cell positioning.
+     * Value is set in user units and is calculated from the top left corner
+     * as origin.
+     *
+     * @var float
+     */
+    var $x;
+
+    /**
+     * The current vertical position for cell positioning.
+     * Value is set in user units and is calculated from the top left corner
+     * as origin.
+     *
+     * @var float
+     */
+    var $y;
+
+    /**
+     * The height of the last cell printed.
+     *
+     * @var float
+     */
+    var $_last_height;
+
+    /**
+     * Line width in user units.
+     *
+     * @var float
+     */
+    var $_line_width;
+
+    /**
+     * An array of standard font names.
+     *
+     * @var array
+     */
+    var $_core_fonts = array('courier'      => 'Courier',
+                             'courierB'     => 'Courier-Bold',
+                             'courierI'     => 'Courier-Oblique',
+                             'courierBI'    => 'Courier-BoldOblique',
+                             'helvetica'    => 'Helvetica',
+                             'helveticaB'   => 'Helvetica-Bold',
+                             'helveticaI'   => 'Helvetica-Oblique',
+                             'helveticaBI'  => 'Helvetica-BoldOblique',
+                             'times'        => 'Times-Roman',
+                             'timesB'       => 'Times-Bold',
+                             'timesI'       => 'Times-Italic',
+                             'timesBI'      => 'Times-BoldItalic',
+                             'symbol'       => 'Symbol',
+                             'zapfdingbats' => 'ZapfDingbats');
+
+    /**
+     * An array of used fonts.
+     *
+     * @var array
+     */
+    var $_fonts = array();
+
+    /**
+     * An array of font files.
+     *
+     * @var array
+     */
+    var $_font_files = array();
+
+    /**
+     * An array of encoding differences.
+     *
+     * @var array
+     */
+    var $_diffs = array();
+
+    /**
+     * An array of used images.
+     *
+     * @var array
+     */
+    var $_images = array();
+
+    /**
+     * An array of links in pages.
+     *
+     * @var array
+     */
+    var $_page_links;
+
+    /**
+     * An array of internal links.
+     *
+     * @var array
+     */
+    var $_links = array();
+
+    /**
+     * Current font family.
+     *
+     * @var string
+     */
+    var $_font_family = '';
+
+    /**
+     * Current font style.
+     *
+     * @var string
+     */
+    var $_font_style = '';
+
+    /**
+     * Underlining flag.
+     *
+     * @var boolean
+     */
+    var $_underline = false;
+
+    /**
+     * An array containing current font info.
+     *
+     * @var array
+     */
+    var $_current_font;
+
+    /**
+     * Current font size in points.
+     *
+     * @var float
+     */
+    var $_font_size_pt = 12;
+
+    /**
+     * Current font size in user units.
+     *
+     * @var float
+     */
+    var $_font_size = 12;
+
+    /**
+     * Commands for filling color.
+     *
+     * @var string
+     */
+    var $_fill_color = '0 g';
+
+    /**
+     * Commands for text color.
+     *
+     * @var string
+     */
+    var $_text_color = '0 g';
+
+    /**
+     * Whether text color is different from fill color.
+     *
+     * @var boolean
+     */
+    var $_color_flag = false;
+
+    /**
+     * Commands for drawing color.
+     *
+     * @var string
+     */
+    var $_draw_color = '0 G';
+
+    /**
+     * Word spacing.
+     *
+     * @var integer
+     */
+    var $_word_spacing = 0;
+
+    /**
+     * Automatic page breaking.
+     *
+     * @var boolean
+     */
+    var $_auto_page_break;
+
+    /**
+     * Threshold used to trigger page breaks.
+     *
+     * @var float
+     */
+    var $_page_break_trigger;
+
+    /**
+     * Flag set when processing footer.
+     *
+     * @var boolean
+     */
+    var $_in_footer = false;
+
+    /**
+     * Zoom display mode.
+     *
+     * @var string
+     */
+    var $_zoom_mode;
+
+    /**
+     * Layout display mode.
+     *
+     * @var string
+     */
+    var $_layout_mode;
+
+    /**
+     * An array containing the document info, consisting of:
+     *   - title
+     *   - subject
+     *   - author
+     *   - keywords
+     *   - creator
+     *
+     * @var array
+     */
+    var $_info = array();
+
+    /**
+     * Alias for total number of pages.
+     *
+     * @var string
+     */
+    var $_alias_nb_pages = '{nb}';
+
+    /**
+     * Attempts to return a conrete PDF instance.
+     *
+     * It allows to set up the page format, the orientation and the units of
+     * measurement used in all the methods (except for the font sizes).
+     *
+     * Example:
+     * <code>
+     * $pdf = File_PDF::factory(array('orientation' => 'P',
+     *                                'unit' => 'mm',
+     *                                'format' => 'A4'));
+     * </code>
+     *
+     * @param array $params  A hash with parameters for the created PDF object.
+     *                       Possible parameters are:
+     *                       - orientation - Default page orientation. Possible
+     *                         values are (case insensitive):
+     *                         - P or Portrait (default)
+     *                         - L or Landscape
+     *                       - unit - User measure units. Possible values
+     *                         values are:
+     *                         - pt: point
+     *                         - mm: millimeter (default)
+     *                         - cm: centimeter
+     *                         - in: inch
+     *                         A point equals 1/72 of inch, that is to say
+     *                         about 0.35 mm (an inch being 2.54 cm). This is a
+     *                         very common unit in typography; font sizes are
+     *                         expressed in that unit.
+     *                       - format - The format used for pages. It can be
+     *                         either one of the following values (case
+     *                         insensitive):
+     *                         - A3
+     *                         - A4 (default)
+     *                         - A5
+     *                         - Letter
+     *                         - Legal
+     *                         or a custom format in the form of a two-element
+     *                         array containing the width and the height
+     *                         (expressed in the unit given by the unit
+     *                         parameter).
+     * @param string $class  The concrete class name to return an instance of.
+     *                       Defaults to File_PDF.
+     */
+    function &factory($params = array(), $class = 'File_PDF')
+    {
+        /* Default parameters. */
+        $defaults = array('orientation' => 'P',
+                          'unit' => 'mm',
+                          'format' => 'A4');
+
+        /* Backward compatibility with old method signature. */
+        /* Should be removed a few versions later. */
+        if (!is_array($params)) {
+            $class = 'File_PDF';
+            $params = $defaults;
+            $names = array_keys($defaults);
+            for ($i = 0; $i < func_num_args(); $i++) {
+                $params[$names[$i]] = func_get_arg($i);
+            }
+        } else {
+            $params = array_merge($defaults, $params);
+        }
+
+        /* Create the PDF object. */
+        $pdf = new $class($params);
+
+        /* Scale factor. */
+        if ($params['unit'] == 'pt') {
+            $pdf->_scale = 1;
+        } elseif ($params['unit'] == 'mm') {
+            $pdf->_scale = 72 / 25.4;
+        } elseif ($params['unit'] == 'cm') {
+            $pdf->_scale = 72 / 2.54;
+        } elseif ($params['unit'] == 'in') {
+            $pdf->_scale = 72;
+        } else {
+            $error = File_PDF::raiseError(sprintf('Incorrect units: %s', $params['unit']));
+            return $error;
+        }
+        /* Page format. */
+        if (is_string($params['format'])) {
+            $params['format'] = strtolower($params['format']);
+            if ($params['format'] == 'a3') {
+                $params['format'] = array(841.89, 1190.55);
+            } elseif ($params['format'] == 'a4') {
+                $params['format'] = array(595.28, 841.89);
+            } elseif ($params['format'] == 'a5') {
+                $params['format'] = array(420.94, 595.28);
+            } elseif ($params['format'] == 'letter') {
+                $params['format'] = array(612, 792);
+            } elseif ($params['format'] == 'legal') {
+                $params['format'] = array(612, 1008);
+            } else {
+                $error = File_PDF::raiseError(sprintf('Unknown page format: %s', $params['format']));
+                return $error;
+            }
+            $pdf->fwPt = $params['format'][0];
+            $pdf->fhPt = $params['format'][1];
+        } else {
+            $pdf->fwPt = $params['format'][0] * $pdf->_scale;
+            $pdf->fhPt = $params['format'][1] * $pdf->_scale;
+        }
+        $pdf->fw = $pdf->fwPt / $pdf->_scale;
+        $pdf->fh = $pdf->fhPt / $pdf->_scale;
+
+        /* Page orientation. */
+        $params['orientation'] = strtolower($params['orientation']);
+        if ($params['orientation'] == 'p' || $params['orientation'] == 'portrait') {
+            $pdf->_default_orientation = 'P';
+            $pdf->wPt = $pdf->fwPt;
+            $pdf->hPt = $pdf->fhPt;
+        } elseif ($params['orientation'] == 'l' || $params['orientation'] == 'landscape') {
+            $pdf->_default_orientation = 'L';
+            $pdf->wPt = $pdf->fhPt;
+            $pdf->hPt = $pdf->fwPt;
+        } else {
+            $error = File_PDF::raiseError(sprintf('Incorrect orientation: %s', $params['orientation']));
+            return $error;
+        }
+        $pdf->_current_orientation = $pdf->_default_orientation;
+        $pdf->w = $pdf->wPt / $pdf->_scale;
+        $pdf->h = $pdf->hPt / $pdf->_scale;
+
+        /* Page margins (1 cm) */
+        $margin = 28.35 / $pdf->_scale;
+        $pdf->setMargins($margin, $margin);
+
+        /* Interior cell margin (1 mm) */
+        $pdf->_cell_margin = $margin / 10;
+
+        /* Line width (0.2 mm) */
+        $pdf->_line_width = .567 / $pdf->_scale;
+
+        /* Automatic page break */
+        $pdf->setAutoPageBreak(true, 2 * $margin);
+
+        /* Full width display mode */
+        $pdf->setDisplayMode('fullwidth');
+
+        /* Compression */
+        $pdf->setCompression(true);
+
+        return $pdf;
+    }
+
+    /**
+     * Returns a PEAR_Error object.
+     *
+     * Wraps around PEAR::raiseError() to avoid having to include PEAR.php
+     * unless an error occurs.
+     *
+     * @param mixed $error  The error message.
+     *
+     * @return object PEAR_Error
+     */
+    function raiseError($error)
+    {
+        require_once 'PEAR.php';
+        return PEAR::raiseError($error);
+    }
+
+    /**
+     * Defines the left, top and right margins.
+     *
+     * By default, they equal 1 cm. Call this method to change them.
+     *
+     * @param float $left   Left margin.
+     * @param float $top    Top margin.
+     * @param float $right  Right margin. If not specified default to the value
+     *                      of the left one.
+     *
+     * @see setAutoPageBreak()
+     * @see setLeftMargin()
+     * @see setRightMargin()
+     * @see setTopMargin()
+     */
+    function setMargins($left, $top, $right = null)
+    {
+        /* Set left and top margins. */
+        $this->_left_margin  = $left;
+        $this->_top_margin   = $top;
+        /* If no right margin set default to same as left. */
+        $this->_right_margin = (is_null($right) ? $left : $right);
+    }
+
+    /**
+     * Defines the left margin.
+     *
+     * The method can be called before creating the first page.  If the
+     * current abscissa gets out of page, it is brought back to the margin.
+     *
+     * @param float $margin  The margin.
+     *
+     * @see setAutoPageBreak()
+     * @see setMargins()
+     * @see setRightMargin()
+     * @see setTopMargin()
+     */
+    function setLeftMargin($margin)
+    {
+        $this->_left_margin = $margin;
+        /* If there is a current page and the current X position is less than
+         * margin set the X position to the margin value. */
+        if ($this->_page > 0 && $this->x < $margin) {
+            $this->x = $margin;
+        }
+    }
+
+    /**
+     * Defines the top margin.
+     *
+     * The method can be called before creating the first page.
+     *
+     * @param float $margin  The margin.
+     */
+    function setTopMargin($margin)
+    {
+        $this->_top_margin = $margin;
+    }
+
+    /**
+     * Defines the right margin.
+     *
+     * The method can be called before creating the first page.
+     *
+     * @param float $margin  The margin.
+     */
+    function setRightMargin($margin)
+    {
+        $this->_right_margin = $margin;
+    }
+
+    /**
+     * Returns the actual page width.
+     *
+     * @since File_PDF 0.2.0
+     * @since Horde 3.2
+     *
+     * @return float  The page width.
+     */
+    function getPageWidth()
+    {
+        return ($this->w - $this->_right_margin - $this->_left_margin);
+    }
+
+    /**
+     * Returns the actual page height.
+     *
+     * @since File_PDF 0.2.0
+     * @since Horde 3.2
+     *
+     * @return float  The page height.
+     */
+    function getPageHeight()
+    {
+        return ($this->h - $this->_top_margin - $this->_break_margin);
+    }
+
+    /**
+     * Enables or disables the automatic page breaking mode.
+     *
+     * When enabling, the second parameter is the distance from the bottom of
+     * the page that defines the triggering limit. By default, the mode is on
+     * and the margin is 2 cm.
+     *
+     * @param boolean $auto  Boolean indicating if mode should be on or off.
+     * @param float $margin  Distance from the bottom of the page.
+     */
+    function setAutoPageBreak($auto, $margin = 0)
+    {
+        $this->_auto_page_break    = $auto;
+        $this->_break_margin       = $margin;
+        $this->_page_break_trigger = $this->h - $margin;
+    }
+
+    /**
+     * Defines the way the document is to be displayed by the viewer.
+     *
+     * The zoom level can be set: pages can be displayed entirely on screen,
+     * occupy the full width of the window, use real size, be scaled by a
+     * specific zooming factor or use viewer default (configured in the
+     * Preferences menu of Acrobat). The page layout can be specified too:
+     * single at once, continuous display, two columns or viewer default.  By
+     * default, documents use the full width mode with continuous display.
+     *
+     * @param mixed $zoom    The zoom to use. It can be one of the following
+     *                       string values:
+     *                         - fullpage: entire page on screen
+     *                         - fullwidth: maximum width of window
+     *                         - real: uses real size (100% zoom)
+     *                         - default: uses viewer default mode
+     *                       or a number indicating the zooming factor.
+     * @param string layout  The page layout. Possible values are:
+     *                         - single: one page at once
+     *                         - continuous: pages in continuously
+     *                         - two: two pages on two columns
+     *                         - default: uses viewer default mode
+     *                       Default value is continuous.
+     */
+    function setDisplayMode($zoom, $layout = 'continuous')
+    {
+        $zoom = strtolower($zoom);
+        if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real'
+            || $zoom == 'default' || !is_string($zoom)) {
+            $this->_zoom_mode = $zoom;
+        } elseif ($zoom == 'zoom') {
+            $this->_zoom_mode = $layout;
+        } else {
+            return $this->raiseError(sprintf('Incorrect zoom display mode: %s', $zoom));
+        }
+
+        $layout = strtolower($layout);
+        if ($layout == 'single' || $layout == 'continuous' || $layout == 'two'
+            || $layout == 'default') {
+            $this->_layout_mode = $layout;
+        } elseif ($zoom != 'zoom') {
+            return $this->raiseError(sprintf('Incorrect layout display mode: %s', $layout));
+        }
+    }
+
+    /**
+     * Activates or deactivates page compression.
+     *
+     * When activated, the internal representation of each page is compressed,
+     * which leads to a compression ratio of about 2 for the resulting
+     * document. Compression is on by default.
+     *
+     * Note: the {@link http://www.php.net/zlib/ zlib extension} is required
+     * for this feature. If not present, compression will be turned off.
+     *
+     * @param boolean $compress  Boolean indicating if compression must be
+     *                           enabled or not.
+     */
+    function setCompression($compress)
+    {
+        /* If no gzcompress function is available then default to false. */
+        $this->_compress = (function_exists('gzcompress') ? $compress : false);
+    }
+
+    /**
+     * Set the info to a document.
+     *
+     * Possible info settings are:
+     *   - title
+     *   - subject
+     *   - author
+     *   - keywords
+     *   - creator
+     *
+     * @param array|string $info  If passed as an array then the complete hash
+     *                            containing the info to be inserted into the
+     *                            document. Otherwise the name of setting to be
+     *                            set.
+     * @param string $value       The value of the setting.
+     */
+    function setInfo($info, $value = '')
+    {
+        if (is_array($info)) {
+            $this->_info = $info;
+        } else {
+            $this->_info[$info] = $value;
+        }
+    }
+
+    /**
+     * Defines an alias for the total number of pages.
+     *
+     * It will be substituted as the document is closed.
+     *
+     * Example:
+     * <code>
+     * class My_File_PDF extends File_PDF {
+     *     function footer()
+     *     {
+     *         // Go to 1.5 cm from bottom
+     *         $this->setY(-15);
+     *         // Select Arial italic 8
+     *         $this->setFont('Arial', 'I', 8);
+     *         // Print current and total page numbers
+     *         $this->cell(0, 10, 'Page ' . $this->getPageNo() . '/{nb}', 0,
+     *                     0, 'C');
+     *     }
+     * }
+     * $pdf = My_File_PDF::factory();
+     * $pdf->aliasNbPages();
+     * </code>
+     *
+     * @param string $alias  The alias.
+     *
+     * @see getPageNo()
+     * @see footer()
+     */
+    function aliasNbPages($alias = '{nb}')
+    {
+        $this->_alias_nb_pages = $alias;
+    }
+
+    /**
+     * This method begins the generation of the PDF document; it must be
+     * called before any output commands.
+     *
+     * No page is created by this method, therefore it is necessary to call
+     * {@link addPage()}.
+     *
+     * @see addPage()
+     * @see close()
+     */
+    function open()
+    {
+        $this->_beginDoc();
+    }
+
+    /**
+     * Terminates the PDF document. It is not necessary to call this method
+     * explicitly because {@link output()} does it automatically.
+     *
+     * If the document contains no page, {@link addPage()} is called to
+     * prevent from getting an invalid document.
+     *
+     * @see open()
+     * @see output()
+     */
+    function close()
+    {
+        /* Terminate document */
+        if ($this->_page == 0) {
+            $result = $this->addPage();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        /* Page footer */
+        $this->_in_footer = true;
+        $this->x = $this->_left_margin;
+        $this->footer();
+        $this->_in_footer = false;
+        /* Close page */
+        $this->_endPage();
+        /* Close document */
+        $this->_endDoc();
+    }
+
+    /**
+     * Adds a new page to the document.
+     *
+     * If a page is already present, the {@link footer()} method is called
+     * first to output the footer. Then the page is added, the current
+     * position set to the top-left corner according to the left and top
+     * margins, and {@link header()} is called to display the header.
+     *
+     * The font which was set before calling is automatically restored. There
+     * is no need to call {@link setFont()} again if you want to continue with
+     * the same font. The same is true for colors and line width.  The origin
+     * of the coordinate system is at the top-left corner and increasing
+     * ordinates go downwards.
+     *
+     * @param string $orientation  Page orientation. Possible values
+     *                             are (case insensitive):
+     *                               - P or Portrait
+     *                               - L or Landscape
+     *                             The default value is the one passed to the
+     *                             constructor.
+     *
+     * @see header()
+     * @see footer()
+     * @see setMargins()
+     */
+    function addPage($orientation = '')
+    {
+        /* For good measure make sure this is called. */
+        $this->_beginDoc();
+
+        /* Save style settings so that they are not overridden by footer() or
+         * header(). */
+        $lw = $this->_line_width;
+        $dc = $this->_draw_color;
+        $fc = $this->_fill_color;
+        $tc = $this->_text_color;
+        $cf = $this->_color_flag;
+        $font_family = $this->_font_family;
+        $font_style = $this->_font_style . ($this->_underline ? 'U' : '');
+        $font_size  = $this->_font_size_pt;
+
+        /* Close old page. */
+        if ($this->_page > 0) {
+            /* Page footer. */
+            $this->_in_footer = true;
+            $this->x = $this->_left_margin;
+            $this->footer();
+            $this->_in_footer = false;
+
+            /* Close page. */
+            $this->_endPage();
+        }
+
+        /* Start new page. */
+        $this->_beginPage($orientation);
+        /* Set line cap style to square. */
+        $this->_out('2 J');
+        /* Set line width. */
+        $this->_line_width = $lw;
+        $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $lw * $this->_scale));
+
+        /* Force the setting of the font. Each new page requires a new
+         * call. */
+        if ($font_family) {
+            $result = $this->setFont($font_family, $font_style, $font_size, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        /* Restore styles. */
+        if ($this->_fill_color != $fc) {
+            $this->_fill_color = $fc;
+            $this->_out($this->_fill_color);
+        }
+        if ($this->_draw_color != $dc) {
+            $this->_draw_color = $dc;
+            $this->_out($this->_draw_color);
+        }
+        $this->_text_color = $tc;
+        $this->_color_flag = $cf;
+
+        /* Page header. */
+        $this->header();
+
+        /* Restore styles. */
+        if ($this->_line_width != $lw) {
+            $this->_line_width = $lw;
+            $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $lw * $this->_scale));
+        }
+        $result = $this->setFont($font_family, $font_style, $font_size);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        if ($this->_fill_color != $fc) {
+            $this->_fill_color = $fc;
+            $this->_out($this->_fill_color);
+        }
+        if ($this->_draw_color != $dc) {
+            $this->_draw_color = $dc;
+            $this->_out($this->_draw_color);
+        }
+        $this->_text_color = $tc;
+        $this->_color_flag = $cf;
+    }
+
+    /**
+     * This method is used to render the page header.
+     *
+     * It is automatically called by {@link addPage()} and should not be
+     * called directly by the application. The implementation in File_PDF:: is
+     * empty, so you have to subclass it and override the method if you want a
+     * specific processing.
+     *
+     * Example:
+     * <code>
+     * class My_File_PDF extends File_PDF {
+     *     function header()
+     *     {
+     *         // Select Arial bold 15
+     *         $this->setFont('Arial', 'B', 15);
+     *         // Move to the right
+     *         $this->cell(80);
+     *         // Framed title
+     *         $this->cell(30, 10, 'Title', 1, 0, 'C');
+     *         // Line break
+     *         $this->newLine(20);
+     *     }
+     * }
+     * </code>
+     *
+     * @see footer()
+     */
+    function header()
+    {
+        /* To be implemented in your own inherited class. */
+    }
+
+    /**
+     * This method is used to render the page footer.
+     *
+     * It is automatically called by {@link addPage()} and {@link close()} and
+     * should not be called directly by the application. The implementation in
+     * File_PDF:: is empty, so you have to subclass it and override the method
+     * if you want a specific processing.
+     *
+     * Example:
+     * <code>
+     * class My_File_PDF extends File_PDF {
+     *    function footer()
+     *    {
+     *        // Go to 1.5 cm from bottom
+     *        $this->setY(-15);
+     *        // Select Arial italic 8
+     *        $this->setFont('Arial', 'I', 8);
+     *        // Print centered page number
+     *        $this->cell(0, 10, 'Page ' . $this->getPageNo(), 0, 0, 'C');
+     *    }
+     * }
+     * </code>
+     *
+     * @see header()
+     */
+    function footer()
+    {
+        /* To be implemented in your own inherited class. */
+    }
+
+    /**
+     * Returns the current page number.
+     *
+     * @return integer
+     *
+     * @see aliasNbPages()
+     */
+    function getPageNo()
+    {
+        return $this->_page;
+    }
+
+    /**
+     * Sets the fill color.
+     *
+     * Depending on the colorspace called, the number of color component
+     * parameters required can be either 1, 3 or 4. The method can be called
+     * before the first page is created and the color is retained from page to
+     * page.
+     *
+     * @param string $cs  Indicates the colorspace which can be either 'rgb',
+     *                    'cmyk' or 'gray'. Defaults to 'rgb'.
+     * @param float $c1   First color component, floating point value between 0
+     *                    and 1. Required for gray, rgb and cmyk.
+     * @param float $c2   Second color component, floating point value
+     *                    between 0 and 1. Required for rgb and cmyk.
+     * @param float $c3   Third color component, floating point value between 0
+     *                    and 1. Required for rgb and cmyk.
+     * @param float $c4   Fourth color component, floating point value
+     *                    between 0 and 1. Required for cmyk.
+     *
+     * @see setTextColor()
+     * @see setDrawColor()
+     * @see rect()
+     * @see cell()
+     * @see multiCell()
+     */
+    function setFillColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0, $c4 = 0)
+    {
+        $cs = strtolower($cs);
+        if ($cs == 'rgb') {
+            $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' rg', $c1, $c2, $c3);
+        } elseif ($cs == 'cmyk') {
+            $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' k', $c1, $c2, $c3, $c4);
+        } else {
+            $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' g', $c1);
+        }
+        if ($this->_page > 0) {
+            $this->_out($this->_fill_color);
+        }
+        $this->_color_flag = $this->_fill_color != $this->_text_color;
+    }
+
+    /**
+     * Sets the text color.
+     *
+     * Depending on the colorspace called, the number of color component
+     * parameters required can be either 1, 3 or 4. The method can be called
+     * before the first page is created and the color is retained from page to
+     * page.
+     *
+     * @param string $cs  Indicates the colorspace which can be either 'rgb',
+     *                    'cmyk' or 'gray'. Defaults to 'rgb'.
+     * @param float $c1   First color component, floating point value between 0
+     *                    and 1. Required for gray, rgb and cmyk.
+     * @param float $c2   Second color component, floating point value
+     *                    between 0 and 1. Required for rgb and cmyk.
+     * @param float $c3   Third color component, floating point value between 0
+     *                    and 1. Required for rgb and cmyk.
+     * @param float $c4   Fourth color component, floating point value
+     *                    between 0 and 1. Required for cmyk.
+     *
+     * @since File_PDF 0.2.0
+     * @since Horde 3.2
+     * @see setFillColor()
+     * @see setDrawColor()
+     * @see rect()
+     * @see cell()
+     * @see multiCell()
+     */
+    function setTextColor($cs, $c1, $c2 = 0, $c3 = 0, $c4 = 0)
+    {
+        $cs = strtolower($cs);
+        if ($cs == 'rgb') {
+            $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' rg', $c1, $c2, $c3);
+        } elseif ($cs == 'cmyk') {
+            $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' k', $c1, $c2, $c3, $c4);
+        } else {
+            $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' g', $c1);
+        }
+        $this->_color_flag = $this->_fill_color != $this->_text_color;
+    }
+
+    /**
+     * Sets the draw color, used when drawing lines.
+     *
+     * Depending on the colorspace called, the number of color component
+     * parameters required can be either 1, 3 or 4. The method can be called
+     * before the first page is created and the color is retained from page to
+     * page.
+     *
+     * @param string $cs  Indicates the colorspace which can be either 'rgb',
+     *                    'cmyk' or 'gray'. Defaults to 'rgb'.
+     * @param float $c1   First color component, floating point value between 0
+     *                    and 1. Required for gray, rgb and cmyk.
+     * @param float $c2   Second color component, floating point value
+     *                    between 0 and 1. Required for rgb and cmyk.
+     * @param float $c3   Third color component, floating point value between 0
+     *                    and 1. Required for rgb and cmyk.
+     * @param float $c4   Fourth color component, floating point value
+     *                    between 0 and 1. Required for cmyk.
+     *
+     * @see setFillColor()
+     * @see line()
+     * @see rect()
+     * @see cell()
+     * @see multiCell()
+     */
+    function setDrawColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0, $c4 = 0)
+    {
+        $cs = strtolower($cs);
+        if ($cs == 'rgb') {
+            $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' RG', $c1, $c2, $c3);
+        } elseif ($cs == 'cmyk') {
+            $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' K', $c1, $c2, $c3, $c4);
+        } else {
+            $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' G', $c1);
+        }
+        if ($this->_page > 0) {
+            $this->_out($this->_draw_color);
+        }
+    }
+
+    /**
+     * Returns the length of a text string. A font must be selected.
+     *
+     * @param string $text  The text whose length is to be computed.
+     * @param boolean $pt   Whether the width should be returned in points or
+     *                      user units.
+     *
+     * @return float
+     */
+    function getStringWidth($text, $pt = false)
+    {
+        $text = (string)$text;
+        $width = 0;
+        $length = strlen($text);
+        for ($i = 0; $i < $length; $i++) {
+            $width += $this->_current_font['cw'][$text{$i}];
+        }
+
+        /* Adjust for word spacing. */
+        $width += $this->_word_spacing * substr_count($text, ' ') * $this->_current_font['cw'][' '];
+
+        if ($pt) {
+            return $width * $this->_font_size_pt / 1000;
+        } else {
+            return $width * $this->_font_size / 1000;
+        }
+    }
+
+    /**
+     * Defines the line width.
+     *
+     * By default, the value equals 0.2 mm. The method can be called before
+     * the first page is created and the value is retained from page to page.
+     *
+     * @param float $width  The width.
+     *
+     * @see line()
+     * @see rect()
+     * @see cell()
+     * @see multiCell()
+     */
+    function setLineWidth($width)
+    {
+        $this->_line_width = $width;
+        if ($this->_page > 0) {
+            $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $width * $this->_scale));
+        }
+    }
+
+    /**
+     * Draws a line between two points.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param float $x1  Abscissa of first point.
+     * @param float $y1  Ordinate of first point.
+     * @param float $x2  Abscissa of second point.
+     * @param float $y2  Ordinate of second point.
+     *
+     * @see setLineWidth()
+     * @see setDrawColor()
+     */
+    function line($x1, $y1, $x2, $y2)
+    {
+        if ($x1 < 0) {
+            $x1 += $this->w;
+        }
+        if ($y1 < 0) {
+            $y1 += $this->h;
+        }
+        if ($x2 < 0) {
+            $x2 += $this->w;
+        }
+        if ($y2 < 0) {
+            $y2 += $this->h;
+        }
+
+        $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S', $x1 * $this->_scale, ($this->h - $y1) * $this->_scale, $x2 * $this->_scale, ($this->h - $y2) * $this->_scale));
+    }
+
+    /**
+     * Outputs a rectangle.
+     *
+     * It can be drawn (border only), filled (with no border) or both.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param float $x       Abscissa of upper-left corner.
+     * @param float $y       Ordinate of upper-left corner.
+     * @param float $width   Width.
+     * @param float $height  Height.
+     * @param float $style   Style of rendering. Possible values are:
+     *                         - D or empty string: draw (default)
+     *                         - F: fill
+     *                         - DF or FD: draw and fill
+     *
+     * @see setLineWidth()
+     * @see setDrawColor()
+     * @see setFillColor()
+     */
+    function rect($x, $y, $width, $height, $style = '')
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        $style = strtoupper($style);
+        if ($style == 'F') {
+            $op = 'f';
+        } elseif ($style == 'FD' || $style == 'DF') {
+            $op = 'B';
+        } else {
+            $op = 'S';
+        }
+
+        $x      = $this->_toPt($x);
+        $y      = $this->_toPt($y);
+        $width  = $this->_toPt($width);
+        $height = $this->_toPt($height);
+
+        $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re %s', $x, $this->hPt - $y, $width, -$height, $op));
+    }
+
+    /**
+     * Outputs a circle. It can be drawn (border only), filled (with no
+     * border) or both.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param float $x       Abscissa of the center of the circle.
+     * @param float $y       Ordinate of the center of the circle.
+     * @param float $r       Circle radius.
+     * @param string $style  Style of rendering. Possible values are:
+     *                         - D or empty string: draw (default)
+     *                         - F: fill
+     *                         - DF or FD: draw and fill
+     */
+    function circle($x, $y, $r, $style = '')
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        $style = strtolower($style);
+        if ($style == 'f') {
+            $op = 'f';      // Style is fill only.
+        } elseif ($style == 'fd' || $style == 'df') {
+            $op = 'B';      // Style is fill and stroke.
+        } else {
+            $op = 'S';      // Style is stroke only.
+        }
+
+        $x = $this->_toPt($x);
+        $y = $this->_toPt($y);
+        $r = $this->_toPt($r);
+
+        /* Invert the y scale. */
+        $y = $this->hPt - $y;
+        /* Length of the Bezier control. */
+        $b = $r * 0.552;
+
+        /* Move from the given origin and set the current point
+         * to the start of the first Bezier curve. */
+        $c = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m', $x - $r, $y);
+        $x = $x - $r;
+        /* First circle quarter. */
+        $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c',
+                      $x, $y + $b,           // First control point.
+                      $x + $r - $b, $y + $r, // Second control point.
+                      $x + $r, $y + $r);     // Final point.
+        /* Set x/y to the final point. */
+        $x = $x + $r;
+        $y = $y + $r;
+        /* Second circle quarter. */
+        $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c',
+                      $x + $b, $y,
+                      $x + $r, $y - $r + $b,
+                      $x + $r, $y - $r);
+        /* Set x/y to the final point. */
+        $x = $x + $r;
+        $y = $y - $r;
+        /* Third circle quarter. */
+        $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c',
+                      $x, $y - $b,
+                      $x - $r + $b, $y - $r,
+                      $x - $r, $y - $r);
+        /* Set x/y to the final point. */
+        $x = $x - $r;
+        $y = $y - $r;
+        /* Fourth circle quarter. */
+        $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c %s',
+                      $x - $b, $y,
+                      $x - $r, $y + $r - $b,
+                      $x - $r, $y + $r,
+                      $op);
+        /* Output the whole string. */
+        $this->_out($c);
+    }
+
+    /**
+     * Imports a TrueType or Type1 font and makes it available. It is
+     * necessary to generate a font definition file first with the
+     * makefont.php utility.
+     * The location of the definition file (and the font file itself when
+     * embedding) must be found at the full path name included.
+     *
+     * Example:
+     * <code>
+     * $pdf->addFont('Comic', 'I');
+     * is equivalent to:
+     * $pdf->addFont('Comic', 'I', 'comici.php');
+     * </code>
+     *
+     * @param string $family  Font family. The name can be chosen arbitrarily.
+     *                        If it is a standard family name, it will
+     *                        override the corresponding font.
+     * @param string $style   Font style. Possible values are (case
+     *                        insensitive):
+     *                          - empty string: regular (default)
+     *                          - B: bold
+     *                          - I: italic
+     *                          - BI or IB: bold italic
+     * @param string $file    The font definition file. By default, the name is
+     *                        built from the family and style, in lower case
+     *                        with no space.
+     *
+     * @see setFont()
+     */
+    function addFont($family, $style = '', $file = '')
+    {
+        $family = strtolower($family);
+        if ($family == 'arial') {
+            $family = 'helvetica';
+        }
+
+        $style = strtoupper($style);
+        if ($style == 'IB') {
+            $style = 'BI';
+        }
+        if (isset($this->_fonts[$family . $style])) {
+            return $this->raiseError(sprintf('Font already added: %s %s', $family, $style));
+        }
+        if ($file == '') {
+            $file = str_replace(' ', '', $family) . strtolower($style) . '.php';
+        }
+        include($file);
+        if (!isset($name)) {
+            return $this->raiseError('Could not include font definition file');
+        }
+        $i = count($this->_fonts) + 1;
+        $this->_fonts[$family . $style] = array('i' => $i, 'type' => $type, 'name' => $name, 'desc' => $desc, 'up' => $up, 'ut' => $ut, 'cw' => $cw, 'enc' => $enc, 'file' => $file);
+        if ($diff) {
+            /* Search existing encodings. */
+            $d = 0;
+            $nb = count($this->_diffs);
+            for ($i = 1; $i <= $nb; $i++) {
+                if ($this->_diffs[$i] == $diff) {
+                    $d = $i;
+                    break;
+                }
+            }
+            if ($d == 0) {
+                $d = $nb + 1;
+                $this->_diffs[$d] = $diff;
+            }
+            $this->_fonts[$family . $style]['diff'] = $d;
+        }
+        if ($file) {
+            if ($type == 'TrueType') {
+                $this->_font_files[$file] = array('length1' => $originalsize);
+            } else {
+                $this->_font_files[$file] = array('length1' => $size1, 'length2' => $size2);
+            }
+        }
+    }
+
+    /**
+     * Sets the font used to print character strings.
+     *
+     * It is mandatory to call this method at least once before printing text
+     * or the resulting document would not be valid. The font can be either a
+     * standard one or a font added via the {@link addFont()} method. Standard
+     * fonts use Windows encoding cp1252 (Western Europe).
+     *
+     * The method can be called before the first page is created and the font
+     * is retained from page to page.
+     *
+     * If you just wish to change the current font size, it is simpler to call
+     * {@link setFontSize()}.
+     *
+     * @param string $family  Family font. It can be either a name defined by
+     *                        {@link addFont()} or one of the standard families
+     *                        (case insensitive):
+     *                          - Courier (fixed-width)
+     *                          - Helvetica or Arial (sans serif)
+     *                          - Times (serif)
+     *                          - Symbol (symbolic)
+     *                          - ZapfDingbats (symbolic)
+     *                        It is also possible to pass an empty string. In
+     *                        that case, the current family is retained.
+     * @param string $style   Font style. Possible values are (case
+     *                        insensitive):
+     *                          - empty string: regular
+     *                          - B: bold
+     *                          - I: italic
+     *                          - U: underline
+     *                        or any combination. Bold and italic styles do not
+     *                        apply to Symbol and ZapfDingbats.
+     * @param integer $size   Font size in points. The default value is the
+     *                        current size. If no size has been specified since
+     *                        the beginning of the document, the value taken
+     *                        is 12.
+     * @param boolean $force  Force the setting of the font. Each new page will
+     *                        require a new call to {@link setFont()} and
+     *                        settings this to true will make sure that the
+     *                        checks for same font calls will be skipped.
+     *
+     * @see addFont()
+     * @see setFontSize()
+     * @see cell()
+     * @see multiCell()
+     * @see write()
+     */
+    function setFont($family, $style = '', $size = null, $force = false)
+    {
+        $family = strtolower($family);
+        if (empty($family)) {
+            $family = $this->_font_family;
+        }
+        if ($family == 'arial') {
+            /* Use helvetica instead of arial. */
+            $family = 'helvetica';
+        } elseif ($family == 'symbol' || $family == 'zapfdingbats') {
+            /* These two fonts do not have styles available. */
+            $style = '';
+        }
+
+        $style = strtoupper($style);
+
+        /* Underline is handled separately, if specified in the style var
+         * remove it from the style and set the underline flag. */
+        if (strpos($style, 'U') !== false) {
+            $this->_underline = true;
+            $style = str_replace('U', '', $style);
+        } else {
+            $this->_underline = false;
+        }
+
+        if ($style == 'IB') {
+            $style = 'BI';
+        }
+
+        /* If no size specified, use current size. */
+        if (is_null($size)) {
+            $size = $this->_font_size_pt;
+        }
+
+        /* If font requested is already the current font and no force setting
+         * of the font is requested (eg. when adding a new page) don't bother
+         * with the rest of the function and simply return. */
+        if ($this->_font_family == $family && $this->_font_style == $style &&
+            $this->_font_size_pt == $size && !$force) {
+            return;
+        }
+
+        /* Set the font key. */
+        $fontkey = $family . $style;
+
+        /* Test if already cached. */
+        if (!isset($this->_fonts[$fontkey])) {
+            /* Get the character width definition file. */
+            $font_widths = File_PDF::_getFontFile($fontkey);
+            if (is_a($font_widths, 'PEAR_Error')) {
+                return $font_widths;
+            }
+
+            $i = count($this->_fonts) + 1;
+            $this->_fonts[$fontkey] = array(
+                'i'    => $i,
+                'type' => 'core',
+                'name' => $this->_core_fonts[$fontkey],
+                'up'   => -100,
+                'ut'   => 50,
+                'cw'   => $font_widths[$fontkey]);
+        }
+
+        /* Store font information as current font. */
+        $this->_font_family  = $family;
+        $this->_font_style   = $style;
+        $this->_font_size_pt = $size;
+        $this->_font_size    = $size / $this->_scale;
+        $this->_current_font = &$this->_fonts[$fontkey];
+
+        /* Output font information if at least one page has been defined. */
+        if ($this->_page > 0) {
+            $this->_out(sprintf('BT /F%d %.2' . FILE_PDF_FLOAT . ' Tf ET', $this->_current_font['i'], $this->_font_size_pt));
+        }
+    }
+
+    /**
+     * Defines the size of the current font.
+     *
+     * @param float $size  The size (in points).
+     *
+     * @see setFont()
+     */
+    function setFontSize($size)
+    {
+        /* If the font size is already the current font size, just return. */
+        if ($this->_font_size_pt == $size) {
+            return;
+        }
+        /* Set the current font size, both in points and scaled to user
+         * units. */
+        $this->_font_size_pt = $size;
+        $this->_font_size = $size / $this->_scale;
+
+        /* Output font information if at least one page has been defined. */
+        if ($this->_page > 0) {
+            $this->_out(sprintf('BT /F%d %.2' . FILE_PDF_FLOAT . ' Tf ET', $this->_current_font['i'], $this->_font_size_pt));
+        }
+    }
+
+    /**
+     * Defines the style of the current font.
+     *
+     * @param string $style  The font style.
+     *
+     * @since File_PDF 0.2.0
+     * @since Horde 3.2
+     * @see setFont()
+     */
+    function setFontStyle($style)
+    {
+        return $this->setFont($this->_font_family, $style);
+    }
+
+    /**
+     * Creates a new internal link and returns its identifier.
+     *
+     * An internal link is a clickable area which directs to another place
+     * within the document.
+     *
+     * The identifier can then be passed to {@link cell()}, {@link()} write,
+     * {@link image()} or {@link link()}. The destination is defined with
+     * {@link setLink()}.
+     *
+     * @see cell()
+     * @see write()
+     * @see image()
+     * @see link()
+     * @see setLink()
+     */
+    function addLink()
+    {
+        $n = count($this->_links) + 1;
+        $this->_links[$n] = array(0, 0);
+        return $n;
+    }
+
+    /**
+     * Defines the page and position a link points to.
+     *
+     * @param integer $link  The link identifier returned by {@link addLink()}.
+     * @param float $y       Ordinate of target position; -1 indicates the
+     *                       current position. The default value is 0 (top of
+     *                       page).
+     * @param integer $page  Number of target page; -1 indicates the current
+     *                       page.
+     *
+     * @see addLink()
+     */
+    function setLink($link, $y = 0, $page = -1)
+    {
+        if ($y == -1) {
+            $y = $this->y;
+        }
+        if ($page == -1) {
+            $page = $this->_page;
+        }
+        $this->_links[$link] = array($page, $y);
+    }
+
+    /**
+     * Puts a link on a rectangular area of the page.
+     *
+     * Text or image links are generally put via {@link cell()}, {@link
+     * write()} or {@link image()}, but this method can be useful for instance
+     * to define a clickable area inside an image.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param float $x       Abscissa of the upper-left corner of the
+     *                       rectangle.
+     * @param float $y       Ordinate of the upper-left corner of the
+     *                       rectangle.
+     * @param float $width   Width of the rectangle.
+     * @param float $height  Height of the rectangle.
+     * @param mixed $link    URL or identifier returned by {@link addLink()}.
+     *
+     * @see addLink()
+     * @see cell()
+     * @see write()
+     * @see image()
+     */
+    function link($x, $y, $width, $height, $link)
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        /* Set up the coordinates with correct scaling in pt. */
+        $x      = $this->_toPt($x);
+        $y      = $this->hPt - $this->_toPt($y);
+        $width  = $this->_toPt($width);
+        $height = $this->_toPt($height);
+
+        /* Save link to page links array. */
+        $this->_link($x, $y, $width, $height, $link);
+    }
+
+    /**
+     * Prints a character string.
+     *
+     * The origin is on the left of the first character, on the baseline. This
+     * method allows to place a string precisely on the page, but it is
+     * usually easier to use {@link cell()}, {@link multiCell()} or {@link
+     * write()} which are the standard methods to print text.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param float $x      Abscissa of the origin.
+     * @param float $y      Ordinate of the origin.
+     * @param string $text  String to print.
+     *
+     * @see setFont()
+     * @see cell()
+     * @see multiCell()
+     * @see write()
+     */
+    function text($x, $y, $text)
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        /* Scale coordinates into points and set correct Y position. */
+        $x = $this->_toPt($x);
+        $y = $this->hPt - $this->_toPt($y);
+
+        /* Escape any potentially harmful characters. */
+        $text = $this->_escape($text);
+
+        $out = sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Td (%s) Tj ET', $x, $y, $text);
+        if ($this->_underline && $text != '') {
+            $out .= ' ' . $this->_doUnderline($x, $y, $text);
+        }
+        if ($this->_color_flag) {
+            $out = sprintf('q %s %s Q', $this->_text_color, $out);
+        }
+        $this->_out($out);
+    }
+
+    /**
+     * Whenever a page break condition is met, the method is called, and the
+     * break is issued or not depending on the returned value. The default
+     * implementation returns a value according to the mode selected by
+     * {@link setAutoPageBreak()}.
+     * This method is called automatically and should not be called directly
+     * by the application.
+     *
+     * @return boolean
+     *
+     * @see setAutoPageBreak()
+     */
+    function acceptPageBreak()
+    {
+        return $this->_auto_page_break;
+    }
+
+    /**
+     * Prints a cell (rectangular area) with optional borders, background
+     * color and character string.
+     *
+     * The upper-left corner of the cell corresponds to the current
+     * position. The text can be aligned or centered. After the call, the
+     * current position moves to the right or to the next line. It is possible
+     * to put a link on the text.  If automatic page breaking is enabled and
+     * the cell goes beyond the limit, a page break is done before outputting.
+     *
+     * @param float $width   Cell width. If 0, the cell extends up to the right
+     *                       margin.
+     * @param float $height  Cell height.
+     * @param string $text   String to print.
+     * @param mixed $border  Indicates if borders must be drawn around the
+     *                       cell. The value can be either a number:
+     *                         - 0: no border (default)
+     *                         - 1: frame
+     *                       or a string containing some or all of the
+     *                       following characters (in any order):
+     *                         - L: left
+     *                         - T: top
+     *                         - R: right
+     *                         - B: bottom
+     * @param integer $ln    Indicates where the current position should go
+     *                       after the call. Possible values are:
+     *                         - 0: to the right (default)
+     *                         - 1: to the beginning of the next line
+     *                         - 2: below
+     *                       Putting 1 is equivalent to putting 0 and calling
+     *                       {@link newLine()} just after.
+     * @param string $align  Allows to center or align the text. Possible
+     *                       values are:
+     *                         - L or empty string: left (default)
+     *                         - C: center
+     *                         - R: right
+     * @param integer $fill  Indicates if the cell fill type. Possible values
+     *                       are:
+     *                         - 0: transparent (default)
+     *                         - 1: painted
+     * @param string $link   URL or identifier returned by {@link addLink()}.
+     *
+     * @see setFont()
+     * @see setDrawColor()
+     * @see setFillColor()
+     * @see setLineWidth()
+     * @see addLink()
+     * @see newLine()
+     * @see multiCell()
+     * @see write()
+     * @see setAutoPageBreak()
+     */
+    function cell($width, $height = 0, $text = '', $border = 0, $ln = 0,
+                  $align = '', $fill = 0, $link = '')
+    {
+        $k = $this->_scale;
+        if ($this->y + $height > $this->_page_break_trigger &&
+            !$this->_in_footer && $this->acceptPageBreak()) {
+            $x = $this->x;
+            $ws = $this->_word_spacing;
+            if ($ws > 0) {
+                $this->_word_spacing = 0;
+                $this->_out('0 Tw');
+            }
+            $result = $this->addPage($this->_current_orientation);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            $this->x = $x;
+            if ($ws > 0) {
+                $this->_word_spacing = $ws;
+                $this->_out(sprintf('%.3' . FILE_PDF_FLOAT . ' Tw', $ws * $k));
+            }
+        }
+        if ($width == 0) {
+            $width = $this->w - $this->_right_margin - $this->x;
+        }
+        $s = '';
+        if ($fill == 1 || $border == 1) {
+            if ($fill == 1) {
+                $op = ($border == 1) ? 'B' : 'f';
+            } else {
+                $op = 'S';
+            }
+            $s = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re %s ', $this->x * $k, ($this->h - $this->y) * $k, $width * $k, -$height * $k, $op);
+        }
+        if (is_string($border)) {
+            if (strpos($border, 'L') !== false) {
+                $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - $this->y) * $k, $this->x * $k, ($this->h - ($this->y + $height)) * $k);
+            }
+            if (strpos($border, 'T') !== false) {
+                $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - $this->y) * $k);
+            }
+            if (strpos($border, 'R') !== false) {
+                $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', ($this->x + $width) * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k);
+            }
+            if (strpos($border, 'B') !== false) {
+                $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - ($this->y + $height)) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k);
+            }
+        }
+        if ($text != '') {
+            if ($align == 'R') {
+                $dx = $width - $this->_cell_margin - $this->getStringWidth($text);
+            } elseif ($align == 'C') {
+                $dx = ($width - $this->getStringWidth($text)) / 2;
+            } else {
+                $dx = $this->_cell_margin;
+            }
+            if ($this->_color_flag) {
+                $s .= 'q ' . $this->_text_color . ' ';
+            }
+            $text = str_replace(')', '\\)', str_replace('(', '\\(', str_replace('\\', '\\\\', $text)));
+            $test2 = ((.5 * $height) + (.3 * $this->_font_size));
+            $test1 = $this->fhPt - (($this->y + $test2) * $k);
+            $x = ($this->x + $dx) * $k;
+            $y = ($this->h - ($this->y + .5 * $height + .3 * $this->_font_size)) * $k;
+            $s .= sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Td (%s) Tj ET', $x, $y, $text);
+            if ($this->_underline) {
+                $s .= ' ' . $this->_doUnderline($x, $y, $text);
+            }
+            if ($this->_color_flag) {
+                $s .= ' Q';
+            }
+            if ($link) {
+                $this->link($this->x + $dx, $this->y + .5 * $height- .5 * $this->_font_size, $this->getStringWidth($text), $this->_font_size, $link);
+            }
+        }
+        if ($s) {
+            $this->_out($s);
+        }
+        $this->_last_height = $height;
+        if ($ln > 0) {
+            /* Go to next line. */
+            $this->y += $height;
+            if ($ln == 1) {
+                $this->x = $this->_left_margin;
+            }
+        } else {
+            $this->x += $width;
+        }
+    }
+
+    /**
+     * This method allows printing text with line breaks.
+     *
+     * They can be automatic (as soon as the text reaches the right border of
+     * the cell) or explicit (via the \n character). As many cells as
+     * necessary are output, one below the other. Text can be aligned,
+     * centered or justified. The cell block can be framed and the background
+     * painted.
+     *
+     * @param float $width   Width of cells. If 0, they extend up to the right
+     *                       margin of the page.
+     * @param float $height  Height of cells.
+     * @param string $text   String to print.
+     * @param mixed $border  Indicates if borders must be drawn around the cell
+     *                       block. The value can be either a number:
+     *                         - 0: no border (default)
+     *                         - 1: frame
+     *                       or a string containing some or all of the
+     *                       following characters (in any order):
+     *                         - L: left
+     *                         - T: top
+     *                         - R: right
+     *                         - B: bottom
+     * @param string $align  Sets the text alignment. Possible values are:
+     *                         - L: left alignment
+     *                         - C: center
+     *                         - R: right alignment
+     *                         - J: justification (default value)
+     * @param integer $fill  Indicates if the cell background must:
+     *                         - 0: transparent (default)
+     *                         - 1: painted
+     *
+     * @see setFont()
+     * @see setDrawColor()
+     * @see setFillColor()
+     * @see setLineWidth()
+     * @see cell()
+     * @see write()
+     * @see setAutoPageBreak()
+     */
+    function multiCell($width, $height, $text, $border = 0, $align = 'J',
+                       $fill = 0)
+    {
+        $cw = &$this->_current_font['cw'];
+        if ($width == 0) {
+            $width = $this->w - $this->_right_margin - $this->x;
+        }
+        $wmax = ($width-2 * $this->_cell_margin) * 1000 / $this->_font_size;
+        $s = str_replace("\r", '', $text);
+        $nb = strlen($s);
+        if ($nb > 0 && $s[$nb-1] == "\n") {
+            $nb--;
+        }
+        $b = 0;
+        if ($border) {
+            if ($border == 1) {
+                $border = 'LTRB';
+                $b = 'LRT';
+                $b2 = 'LR';
+            } else {
+                $b2 = '';
+                if (strpos($border, 'L') !== false) {
+                    $b2 .= 'L';
+                }
+                if (strpos($border, 'R') !== false) {
+                    $b2 .= 'R';
+                }
+                $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2;
+            }
+        }
+        $sep = -1;
+        $i   = 0;
+        $j   = 0;
+        $l   = 0;
+        $ns  = 0;
+        $nl  = 1;
+        while ($i < $nb) {
+            /* Get next character. */
+            $c = $s[$i];
+            if ($c == "\n") {
+                /* Explicit line break. */
+                if ($this->_word_spacing > 0) {
+                    $this->_word_spacing = 0;
+                    $this->_out('0 Tw');
+                }
+                $result = $this->cell($width, $height, substr($s, $j, $i-$j),
+                                      $b, 2, $align, $fill);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                $i++;
+                $sep = -1;
+                $j = $i;
+                $l = 0;
+                $ns = 0;
+                $nl++;
+                if ($border && $nl == 2) {
+                    $b = $b2;
+                }
+                continue;
+            }
+            if ($c == ' ') {
+                $sep = $i;
+                $ls = $l;
+                $ns++;
+            }
+            $l += $cw[$c];
+            if ($l > $wmax) {
+                /* Automatic line break. */
+                if ($sep == -1) {
+                    if ($i == $j) {
+                        $i++;
+                    }
+                    if ($this->_word_spacing > 0) {
+                        $this->_word_spacing = 0;
+                        $this->_out('0 Tw');
+                    }
+                    $result = $this->cell($width, $height,
+                                          substr($s, $j, $i - $j), $b, 2,
+                                          $align, $fill);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                } else {
+                    if ($align == 'J') {
+                        $this->_word_spacing = ($ns>1)
+                            ? ($wmax - $ls) / 1000 * $this->_font_size / ($ns - 1)
+                            : 0;
+                        $this->_out(sprintf('%.3' . FILE_PDF_FLOAT . ' Tw',
+                                            $this->_word_spacing * $this->_scale));
+                    }
+                    $result = $this->cell($width, $height,
+                                          substr($s, $j, $sep - $j),
+                                          $b, 2, $align, $fill);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                    $i = $sep + 1;
+                }
+                $sep = -1;
+                $j = $i;
+                $l = 0;
+                $ns = 0;
+                $nl++;
+                if ($border && $nl == 2) {
+                    $b = $b2;
+                }
+            } else {
+                $i++;
+            }
+        }
+        /* Last chunk. */
+        if ($this->_word_spacing > 0) {
+            $this->_word_spacing = 0;
+            $this->_out('0 Tw');
+        }
+        if ($border && strpos($border, 'B') !== false) {
+            $b .= 'B';
+        }
+        $result = $this->cell($width, $height, substr($s, $j, $i), $b, 2,
+                              $align, $fill);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $this->x = $this->_left_margin;
+    }
+
+    /**
+     * This method prints text from the current position.
+     *
+     * When the right margin is reached (or the \n character is met) a line
+     * break occurs and text continues from the left margin. Upon method exit,
+     * the current position is left just at the end of the text.
+     *
+     * It is possible to put a link on the text.
+     *
+     * Example:
+     * <code>
+     * // Begin with regular font
+     * $pdf->setFont('Arial', '', 14);
+     * $pdf->write(5, 'Visit ');
+     * // Then put a blue underlined link
+     * $pdf->setTextColor(0, 0, 255);
+     * $pdf->setFont('', 'U');
+     * $pdf->write(5, 'www.fpdf.org', 'http://www.fpdf.org');
+     * </code>
+     *
+     * @param float $height  Line height.
+     * @param string $text   String to print.
+     * @param mixed $link    URL or identifier returned by {@link addLink()}.
+     *
+     * @see setFont()
+     * @see addLink()
+     * @see multiCell()
+     * @see setAutoPageBreak()
+     */
+    function write($height, $text, $link = '')
+    {
+        $cw = &$this->_current_font['cw'];
+        $width = $this->w - $this->_right_margin - $this->x;
+        $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
+        $s = str_replace("\r", '', $text);
+        $nb = strlen($s);
+        $sep = -1;
+        $i = 0;
+        $j = 0;
+        $l = 0;
+        $nl = 1;
+        while ($i < $nb) {
+            /* Get next character. */
+            $c = $s{$i};
+            if ($c == "\n") {
+                /* Explicit line break. */
+                $result = $this->cell($width, $height, substr($s, $j, $i - $j),
+                                      0, 2, '', 0, $link);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                $i++;
+                $sep = -1;
+                $j = $i;
+                $l = 0;
+                if ($nl == 1) {
+                    $this->x = $this->_left_margin;
+                    $width = $this->w - $this->_right_margin - $this->x;
+                    $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
+                }
+                $nl++;
+                continue;
+            }
+            if ($c == ' ') {
+                $sep = $i;
+                $ls = $l;
+            }
+            $l += (isset($cw[$c]) ? $cw[$c] : 0);
+            if ($l > $wmax) {
+                /* Automatic line break. */
+                if ($sep == -1) {
+                    if ($this->x > $this->_left_margin) {
+                        /* Move to next line. */
+                        $this->x = $this->_left_margin;
+                        $this->y += $height;
+                        $width = $this->w - $this->_right_margin - $this->x;
+                        $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
+                        $i++;
+                        $nl++;
+                        continue;
+                    }
+                    if ($i == $j) {
+                        $i++;
+                    }
+                    $result = $this->cell($width, $height,
+                                          substr($s, $j, $i - $j),
+                                          0, 2, '', 0, $link);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                } else {
+                    $result = $this->cell($width, $height,
+                                          substr($s, $j, $sep - $j),
+                                          0, 2, '', 0, $link);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                    $i = $sep + 1;
+                }
+                $sep = -1;
+                $j = $i;
+                $l = 0;
+                if ($nl == 1) {
+                    $this->x = $this->_left_margin;
+                    $width = $this->w - $this->_right_margin - $this->x;
+                    $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
+                }
+                $nl++;
+            } else {
+                $i++;
+            }
+        }
+        /* Last chunk. */
+        if ($i != $j) {
+            $result = $this->cell($l / 1000 * $this->_font_size, $height,
+                                  substr($s, $j, $i), 0, 0, '', 0, $link);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+    }
+
+    /**
+     * Writes text at an angle.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * @param integer $x         X coordinate.
+     * @param integer $y         Y coordinate.
+     * @param string $text       Text to write.
+     * @param float $text_angle  Angle to rotate (Eg. 90 = bottom to top).
+     * @param float $font_angle  Rotate characters as well as text.
+     *
+     * @see setFont()
+     */
+    function writeRotated($x, $y, $text, $text_angle, $font_angle = 0)
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        /* Escape text. */
+        $text = $this->_escape($text);
+
+        $font_angle += 90 + $text_angle;
+        $text_angle *= M_PI / 180;
+        $font_angle *= M_PI / 180;
+
+        $text_dx = cos($text_angle);
+        $text_dy = sin($text_angle);
+        $font_dx = cos($font_angle);
+        $font_dy = sin($font_angle);
+
+        $s= sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Tm (%s) Tj ET',
+                    $text_dx, $text_dy, $font_dx, $font_dy,
+                    $x * $this->_scale, ($this->h-$y) * $this->_scale, $text);
+
+        if ($this->_draw_color) {
+            $s = 'q ' . $this->_draw_color . ' ' . $s . ' Q';
+        }
+        $this->_out($s);
+    }
+
+    /**
+     * Prints an image in the page.
+     *
+     * The upper-left corner and at least one of the dimensions must be
+     * specified; the height or the width can be calculated automatically in
+     * order to keep the image proportions. Supported formats are JPEG and
+     * PNG.
+     *
+     * All coordinates can be negative to provide values from the right or
+     * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2).
+     *
+     * For JPEG, all flavors are allowed:
+     *   - gray scales
+     *   - true colors (24 bits)
+     *   - CMYK (32 bits)
+     *
+     * For PNG, are allowed:
+     *   - gray scales on at most 8 bits (256 levels)
+     *   - indexed colors
+     *   - true colors (24 bits)
+     * but are not supported:
+     *   - Interlacing
+     *   - Alpha channel
+     *
+     * If a transparent color is defined, it will be taken into account (but
+     * will be only interpreted by Acrobat 4 and above).
+     * The format can be specified explicitly or inferred from the file
+     * extension.
+     * It is possible to put a link on the image.
+     *
+     * Remark: if an image is used several times, only one copy will be
+     * embedded in the file.
+     *
+     * @param string $file   Name of the file containing the image.
+     * @param float $x       Abscissa of the upper-left corner.
+     * @param float $y       Ordinate of the upper-left corner.
+     * @param float $width   Width of the image in the page. If equal to zero,
+     *                       it is automatically calculated to keep the
+     *                       original proportions.
+     * @param float $height  Height of the image in the page. If not specified
+     *                       or equal to zero, it is automatically calculated
+     *                       to keep the original proportions.
+     * @param string $type   Image format. Possible values are (case
+     *                       insensitive): JPG, JPEG, PNG. If not specified,
+     *                       the type is inferred from the file extension.
+     * @param mixed $link    URL or identifier returned by {@link addLink()}.
+     *
+     * @see addLink()
+     */
+    function image($file, $x, $y, $width = 0, $height = 0, $type = '',
+                   $link = '')
+    {
+        if ($x < 0) {
+            $x += $this->w;
+        }
+        if ($y < 0) {
+            $y += $this->h;
+        }
+
+        if (!isset($this->_images[$file])) {
+            /* First use of image, get some file info. */
+            if ($type == '') {
+                $pos = strrpos($file, '.');
+                if ($pos === false) {
+                    return $this->raiseError(sprintf('Image file has no extension and no type was specified: %s', $file));
+                }
+                $type = substr($file, $pos + 1);
+            }
+            $type = strtolower($type);
+            $mqr = get_magic_quotes_runtime();
+            set_magic_quotes_runtime(0);
+            if ($type == 'jpg' || $type == 'jpeg') {
+                $info = $this->_parseJPG($file);
+            } elseif ($type == 'png') {
+                $info = $this->_parsePNG($file);
+            } else {
+                return $this->raiseError(sprintf('Unsupported image file type: %s', $type));
+            }
+            if (is_a($info, 'PEAR_Error')) {
+                return $info;
+            }
+            set_magic_quotes_runtime($mqr);
+            $info['i'] = count($this->_images) + 1;
+            $this->_images[$file] = $info;
+        } else {
+            $info = $this->_images[$file];
+        }
+
+        /* Make sure all vars are converted to pt scale. */
+        $x      = $this->_toPt($x);
+        $y      = $this->hPt - $this->_toPt($y);
+        $width  = $this->_toPt($width);
+        $height = $this->_toPt($height);
+
+        /* If not specified do automatic width and height calculations. */
+        if (empty($width) && empty($height)) {
+            $width = $info['w'];
+            $height = $info['h'];
+        } elseif (empty($width)) {
+            $width = $height * $info['w'] / $info['h'];
+        } elseif (empty($height)) {
+            $height = $width * $info['h'] / $info['w'];
+        }
+
+        $this->_out(sprintf('q %.2' . FILE_PDF_FLOAT . ' 0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' cm /I%d Do Q', $width, $height, $x, $y - $height, $info['i']));
+
+        /* Set any link if requested. */
+        if ($link) {
+            $this->_link($x, $y, $width, $height, $link);
+        }
+    }
+
+    /**
+     * Performs a line break.
+     *
+     * The current abscissa goes back to the left margin and the ordinate
+     * increases by the amount passed in parameter.
+     *
+     * @param float $height  The height of the break. By default, the value
+     *                       equals the height of the last printed cell.
+     *
+     * @see cell()
+     */
+    function newLine($height = '')
+    {
+        $this->x = $this->_left_margin;
+        if (is_string($height)) {
+            $this->y += $this->_last_height;
+        } else {
+            $this->y += $height;
+        }
+    }
+
+    /**
+     * Returns the abscissa of the current position in user units.
+     *
+     * @return float
+     *
+     * @see setX()
+     * @see getY()
+     * @see setY()
+     */
+    function getX()
+    {
+        return $this->x;
+    }
+
+    /**
+     * Defines the abscissa of the current position.
+     *
+     * If the passed value is negative, it is relative to the right of the
+     * page.
+     *
+     * @param float $x  The value of the abscissa.
+     *
+     * @see getX()
+     * @see getY()
+     * @see setY()
+     * @see setXY()
+     */
+    function setX($x)
+    {
+        if ($x >= 0) {
+            /* Absolute value. */
+            $this->x = $x;
+        } else {
+            /* Negative, so relative to right edge of the page. */
+            $this->x = $this->w + $x;
+        }
+    }
+
+    /**
+     * Returns the ordinate of the current position in user units.
+     *
+     * @return float
+     *
+     * @see setY()
+     * @see getX()
+     * @see setX()
+     */
+    function getY()
+    {
+        return $this->y;
+    }
+
+    /**
+     * Defines the ordinate of the current position.
+     *
+     * If the passed value is negative, it is relative to the bottom of the
+     * page.
+     *
+     * @param float $y  The value of the ordinate.
+     *
+     * @see getX()
+     * @see getY()
+     * @see setY()
+     * @see setXY()
+     */
+    function setY($y)
+    {
+        if ($y >= 0) {
+            /* Absolute value. */
+            $this->y = $y;
+        } else {
+            /* Negative, so relative to bottom edge of the page. */
+            $this->y = $this->h + $y;
+        }
+    }
+
+    /**
+     * Defines the abscissa and ordinate of the current position.
+     *
+     * If the passed values are negative, they are relative respectively to
+     * the right and bottom of the page.
+     *
+     * @param float $x  The value of the abscissa.
+     * @param float $y  The value of the ordinate.
+     *
+     * @see setX()
+     * @see setY()
+     */
+    function setXY($x, $y)
+    {
+        $this->setY($y);
+        $this->setX($x);
+    }
+
+    /**
+     * Returns the current buffer content and resets the buffer.
+     *
+     * Use this method when creating large files to avoid memory problems.
+     * This method doesn't work in combination with the output() or save()
+     * methods, use getOutput() at the end. Calling this method doubles the
+     * memory usage during the call.
+     *
+     * @since File_PDF 0.2.0
+     * @since Horde 3.2
+     * @see getOutput()
+     */
+    function flush()
+    {
+        // Make sure we have the file header.
+        $this->_beginDoc();
+
+        $buffer = $this->_buffer;
+        $this->_buffer = '';
+        $this->_flushed = true;
+        $this->_buflen += strlen($buffer);
+
+        return $buffer;
+    }
+
+    /**
+     * Returns the raw PDF file.
+     *
+     * @see output()
+     * @see flush()
+     */
+    function getOutput()
+    {
+        /* Check whether file has been closed. */
+        if ($this->_state < 3) {
+            $result = $this->close();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return $this->_buffer;
+    }
+
+    /**
+     * Sends the buffered data to the browser.
+     *
+     * @param string $filename  The filename for the output file.
+     * @param boolean $inline   True if inline, false if attachment.
+     */
+    function output($filename = 'unknown.pdf', $inline = false)
+    {
+        /* Check whether the buffer has been flushed already. */
+        if ($this->_flushed) {
+            return $this->raiseError('The buffer has been flushed already, don\'t use output() in combination with flush().');
+        }
+
+        /* Check whether file has been closed. */
+        if ($this->_state < 3) {
+            $result = $this->close();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        /* Check if headers have been sent. */
+        if (headers_sent()) {
+            return $this->raiseError('Unable to send PDF file, some data has already been output to browser');
+        }
+
+        /* If HTTP_Download is not available return a PEAR_Error. */
+        if (!include_once 'HTTP/Download.php') {
+            return $this->raiseError('Missing PEAR package HTTP_Download');
+        }
+
+        /* Params for the output. */
+        $disposition = $inline ? HTTP_DOWNLOAD_INLINE : HTTP_DOWNLOAD_ATTACHMENT;
+        $params = array('data'               => $this->_buffer,
+                        'contenttype'        => 'application/pdf',
+                        'contentdisposition' => array($disposition, $filename));
+        /* Output the file. */
+        return HTTP_Download::staticSend($params);
+    }
+
+    /**
+     * Saves the PDF file on the filesystem.
+     *
+     * @param string $filename  The filename for the output file.
+     */
+    function save($filename = 'unknown.pdf')
+    {
+        /* Check whether the buffer has been flushed already. */
+        if ($this->_flushed) {
+            return $this->raiseError('The buffer has been flushed already, don\'t use save() in combination with flush().');
+        }
+
+        /* Check whether file has been closed. */
+        if ($this->_state < 3) {
+            $result = $this->close();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $f = fopen($filename, 'wb');
+        if (!$f) {
+            return $this->raiseError(sprintf('Unable to save PDF file: %s', $filename));
+        }
+        fwrite($f, $this->_buffer, strlen($this->_buffer));
+        fclose($f);
+    }
+
+    function _toPt($val)
+    {
+        return $val * $this->_scale;
+    }
+
+    function _getFontFile($fontkey, $path = '')
+    {
+        static $font_widths = array();
+
+        if (!isset($font_widths[$fontkey])) {
+            if (!empty($path)) {
+                $file = $path . strtolower($fontkey) . '.php';
+            } else {
+                $file = 'File/PDF/fonts/' . strtolower($fontkey) . '.php';
+            }
+            include $file;
+            if (!isset($font_widths[$fontkey])) {
+                return $this->raiseError(sprintf('Could not include font metric file: %s', $file));
+            }
+        }
+
+        return $font_widths;
+    }
+
+    function _link($x, $y, $width, $height, $link)
+    {
+        /* Save link to page links array. */
+        $this->_page_links[$this->_page][] = array($x, $y, $width, $height, $link);
+    }
+
+    function _beginDoc()
+    {
+        /* Start document, but only if not yet started. */
+        if ($this->_state < 1) {
+            $this->_state = 1;
+            $this->_out('%PDF-1.3');
+        }
+    }
+
+    function _putPages()
+    {
+        $nb = $this->_page;
+        if (!empty($this->_alias_nb_pages)) {
+            /* Replace number of pages. */
+            for ($n = 1; $n <= $nb; $n++) {
+                $this->_pages[$n] = str_replace($this->_alias_nb_pages, $nb, $this->_pages[$n]);
+            }
+        }
+        if ($this->_default_orientation == 'P') {
+            $wPt = $this->fwPt;
+            $hPt = $this->fhPt;
+        } else {
+            $wPt = $this->fhPt;
+            $hPt = $this->fwPt;
+        }
+        $filter = ($this->_compress) ? '/Filter /FlateDecode ' : '';
+        for ($n = 1; $n <= $nb; $n++) {
+            /* Page */
+            $this->_newobj();
+            $this->_out('<</Type /Page');
+            $this->_out('/Parent 1 0 R');
+            if (isset($this->_orientation_changes[$n])) {
+                $this->_out(sprintf('/MediaBox [0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ']', $hPt, $wPt));
+            }
+            $this->_out('/Resources 2 0 R');
+            if (isset($this->_page_links[$n])) {
+                /* Links */
+                $annots = '/Annots [';
+                foreach ($this->_page_links[$n] as $pl) {
+                    $rect = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . '', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
+                    $annots .= '<</Type /Annot /Subtype /Link /Rect [' . $rect . '] /Border [0 0 0] ';
+                    if (is_string($pl[4])) {
+                        $annots .= '/A <</S /URI /URI ' . $this->_textString($pl[4]) . '>>>>';
+                    } else {
+                        $l = $this->_links[$pl[4]];
+                        $height = isset($this->_orientation_changes[$l[0]]) ? $wPt : $hPt;
+                        $annots .= sprintf('/Dest [%d 0 R /XYZ 0 %.2' . FILE_PDF_FLOAT . ' null]>>', 1 + 2 * $l[0], $height - $l[1] * $this->_scale);
+                    }
+                }
+                $this->_out($annots . ']');
+            }
+            $this->_out('/Contents ' . ($this->_n + 1) . ' 0 R>>');
+            $this->_out('endobj');
+            /* Page content */
+            $p = ($this->_compress) ? gzcompress($this->_pages[$n]) : $this->_pages[$n];
+            $this->_newobj();
+            $this->_out('<<' . $filter . '/Length ' . strlen($p) . '>>');
+            $this->_putStream($p);
+            $this->_out('endobj');
+        }
+        /* Pages root */
+        $this->_offsets[1] = $this->_buflen + strlen($this->_buffer);
+        $this->_out('1 0 obj');
+        $this->_out('<</Type /Pages');
+        $kids = '/Kids [';
+        for ($i = 0; $i < $nb; $i++) {
+            $kids .= (3 + 2 * $i) . ' 0 R ';
+        }
+        $this->_out($kids . ']');
+        $this->_out('/Count ' . $nb);
+        $this->_out(sprintf('/MediaBox [0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ']', $wPt, $hPt));
+        $this->_out('>>');
+        $this->_out('endobj');
+    }
+
+    function _putFonts()
+    {
+        $nf = $this->_n;
+        foreach ($this->_diffs as $diff) {
+            /* Encodings */
+            $this->_newobj();
+            $this->_out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' . $diff . ']>>');
+            $this->_out('endobj');
+        }
+        $mqr = get_magic_quotes_runtime();
+        set_magic_quotes_runtime(0);
+        foreach ($this->_font_files as $file => $info) {
+            /* Font file embedding. */
+            $this->_newobj();
+            $this->_font_files[$file]['n'] = $this->_n;
+            $size = filesize($file);
+            if (!$size) {
+                return $this->raiseError('Font file not found');
+            }
+            $this->_out('<</Length ' . $size);
+            if (substr($file, -2) == '.z') {
+                $this->_out('/Filter /FlateDecode');
+            }
+            $this->_out('/Length1 ' . $info['length1']);
+            if (isset($info['length2'])) {
+                $this->_out('/Length2 ' . $info['length2'] . ' /Length3 0');
+            }
+            $this->_out('>>');
+            $f = fopen($file, 'rb');
+            $this->_putStream(fread($f, $size));
+            fclose($f);
+            $this->_out('endobj');
+        }
+        set_magic_quotes_runtime($mqr);
+        foreach ($this->_fonts as $k => $font) {
+            /* Font objects */
+            $this->_newobj();
+            $this->_fonts[$k]['n'] = $this->_n;
+            $name = $font['name'];
+            $this->_out('<</Type /Font');
+            $this->_out('/BaseFont /' . $name);
+            if ($font['type'] == 'core') {
+                /* Standard font. */
+                $this->_out('/Subtype /Type1');
+                if ($name != 'Symbol' && $name != 'ZapfDingbats') {
+                    $this->_out('/Encoding /WinAnsiEncoding');
+                }
+            } else {
+                /* Additional font. */
+                $this->_out('/Subtype /' . $font['type']);
+                $this->_out('/FirstChar 32');
+                $this->_out('/LastChar 255');
+                $this->_out('/Widths ' . ($this->_n + 1) . ' 0 R');
+                $this->_out('/FontDescriptor ' . ($this->_n + 2) . ' 0 R');
+                if ($font['enc']) {
+                    if (isset($font['diff'])) {
+                        $this->_out('/Encoding ' . ($nf + $font['diff']) . ' 0 R');
+                    } else {
+                        $this->_out('/Encoding /WinAnsiEncoding');
+                    }
+                }
+            }
+            $this->_out('>>');
+            $this->_out('endobj');
+            if ($font['type'] != 'core') {
+                /* Widths. */
+                $this->_newobj();
+                $cw = &$font['cw'];
+                $s = '[';
+                for ($i = 32; $i <= 255; $i++) {
+                    $s .= $cw[chr($i)] . ' ';
+                }
+                $this->_out($s . ']');
+                $this->_out('endobj');
+                /* Descriptor. */
+                $this->_newobj();
+                $s = '<</Type /FontDescriptor /FontName /' . $name;
+                foreach ($font['desc'] as $k => $v) {
+                    $s .= ' /' . $k . ' ' . $v;
+                }
+                $file = $font['file'];
+                if ($file) {
+                    $s .= ' /FontFile' . ($font['type'] == 'Type1' ? '' : '2') . ' ' . $this->_font_files[$file]['n'] . ' 0 R';
+                }
+                $this->_out($s . '>>');
+                $this->_out('endobj');
+            }
+        }
+    }
+
+    function _putImages()
+    {
+        $filter = ($this->_compress) ? '/Filter /FlateDecode ' : '';
+        foreach ($this->_images as $file => $info) {
+            $this->_newobj();
+            $this->_images[$file]['n'] = $this->_n;
+            $this->_out('<</Type /XObject');
+            $this->_out('/Subtype /Image');
+            $this->_out('/Width ' . $info['w']);
+            $this->_out('/Height ' . $info['h']);
+            if ($info['cs'] == 'Indexed') {
+                $this->_out('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal'])/3 - 1) . ' ' . ($this->_n + 1) . ' 0 R]');
+            } else {
+                $this->_out('/ColorSpace /' . $info['cs']);
+                if ($info['cs'] == 'DeviceCMYK') {
+                    $this->_out('/Decode [1 0 1 0 1 0 1 0]');
+                }
+            }
+            $this->_out('/BitsPerComponent ' . $info['bpc']);
+            $this->_out('/Filter /' . $info['f']);
+            if (isset($info['parms'])) {
+                $this->_out($info['parms']);
+            }
+            if (isset($info['trns']) && is_array($info['trns'])) {
+                $trns = '';
+                $i_max = count($info['trns']);
+                for ($i = 0; $i < $i_max; $i++) {
+                    $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' ';
+                }
+                $this->_out('/Mask [' . $trns . ']');
+            }
+            $this->_out('/Length ' . strlen($info['data']) . '>>');
+            $this->_putStream($info['data']);
+            $this->_out('endobj');
+
+            /* Palette. */
+            if ($info['cs'] == 'Indexed') {
+                $this->_newobj();
+                $pal = ($this->_compress) ? gzcompress($info['pal']) : $info['pal'];
+                $this->_out('<<' . $filter . '/Length ' . strlen($pal) . '>>');
+                $this->_putStream($pal);
+                $this->_out('endobj');
+            }
+        }
+    }
+
+    function _putResources()
+    {
+        $this->_putFonts();
+        $this->_putImages();
+        /* Resource dictionary */
+        $this->_offsets[2] = $this->_buflen + strlen($this->_buffer);
+        $this->_out('2 0 obj');
+        $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
+        $this->_out('/Font <<');
+        foreach ($this->_fonts as $font) {
+            $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');
+        }
+        $this->_out('>>');
+        if (count($this->_images)) {
+            $this->_out('/XObject <<');
+            foreach ($this->_images as $image) {
+                $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');
+            }
+            $this->_out('>>');
+        }
+        $this->_out('>>');
+        $this->_out('endobj');
+    }
+
+    function _putInfo()
+    {
+        $this->_out('/Producer ' . $this->_textString('Horde PDF'));
+        if (!empty($this->_info['title'])) {
+            $this->_out('/Title ' . $this->_textString($this->_info['title']));
+        }
+        if (!empty($this->_info['subject'])) {
+            $this->_out('/Subject ' . $this->_textString($this->_info['subject']));
+        }
+        if (!empty($this->_info['author'])) {
+            $this->_out('/Author ' . $this->_textString($this->_info['author']));
+        }
+        if (!empty($this->keywords)) {
+            $this->_out('/Keywords ' . $this->_textString($this->keywords));
+        }
+        if (!empty($this->creator)) {
+            $this->_out('/Creator ' . $this->_textString($this->creator));
+        }
+        $this->_out('/CreationDate ' . $this->_textString('D:' . date('YmdHis')));
+    }
+
+    function _putCatalog()
+    {
+        $this->_out('/Type /Catalog');
+        $this->_out('/Pages 1 0 R');
+        if ($this->_zoom_mode == 'fullpage') {
+            $this->_out('/OpenAction [3 0 R /Fit]');
+        } elseif ($this->_zoom_mode == 'fullwidth') {
+            $this->_out('/OpenAction [3 0 R /FitH null]');
+        } elseif ($this->_zoom_mode == 'real') {
+            $this->_out('/OpenAction [3 0 R /XYZ null null 1]');
+        } elseif (!is_string($this->_zoom_mode)) {
+            $this->_out('/OpenAction [3 0 R /XYZ null null ' . ($this->_zoom_mode / 100) . ']');
+        }
+        if ($this->_layout_mode == 'single') {
+            $this->_out('/PageLayout /SinglePage');
+        } elseif ($this->_layout_mode == 'continuous') {
+            $this->_out('/PageLayout /OneColumn');
+        } elseif ($this->_layout_mode == 'two') {
+            $this->_out('/PageLayout /TwoColumnLeft');
+        }
+    }
+
+    function _putTrailer()
+    {
+        $this->_out('/Size ' . ($this->_n + 1));
+        $this->_out('/Root ' . $this->_n . ' 0 R');
+        $this->_out('/Info ' . ($this->_n - 1) . ' 0 R');
+    }
+
+    function _endDoc()
+    {
+        $this->_putPages();
+        $this->_putResources();
+        /* Info */
+        $this->_newobj();
+        $this->_out('<<');
+        $this->_putInfo();
+        $this->_out('>>');
+        $this->_out('endobj');
+        /* Catalog */
+        $this->_newobj();
+        $this->_out('<<');
+        $this->_putCatalog();
+        $this->_out('>>');
+        $this->_out('endobj');
+        /* Cross-ref */
+        $o = $this->_buflen + strlen($this->_buffer);
+        $this->_out('xref');
+        $this->_out('0 ' . ($this->_n + 1));
+        $this->_out('0000000000 65535 f ');
+        for ($i = 1; $i <= $this->_n; $i++) {
+            $this->_out(sprintf('%010d 00000 n ', $this->_offsets[$i]));
+        }
+        /* Trailer */
+        $this->_out('trailer');
+        $this->_out('<<');
+        $this->_putTrailer();
+        $this->_out('>>');
+        $this->_out('startxref');
+        $this->_out($o);
+        $this->_out('%%EOF');
+        $this->_state = 3;
+    }
+
+    function _beginPage($orientation)
+    {
+        $this->_page++;
+        $this->_pages[$this->_page] = '';
+        $this->_state = 2;
+        $this->x = $this->_left_margin;
+        $this->y = $this->_top_margin;
+        $this->_last_height = 0;
+        /* Page orientation */
+        if (!$orientation) {
+            $orientation = $this->_default_orientation;
+        } else {
+            $orientation = strtoupper($orientation[0]);
+            if ($orientation != $this->_default_orientation) {
+                $this->_orientation_changes[$this->_page] = true;
+            }
+        }
+        if ($orientation != $this->_current_orientation) {
+            /* Change orientation */
+            if ($orientation == 'P') {
+                $this->wPt = $this->fwPt;
+                $this->hPt = $this->fhPt;
+                $this->w   = $this->fw;
+                $this->h   = $this->fh;
+            } else {
+                $this->wPt = $this->fhPt;
+                $this->hPt = $this->fwPt;
+                $this->w   = $this->fh;
+                $this->h   = $this->fw;
+            }
+            $this->_page_break_trigger = $this->h - $this->_break_margin;
+            $this->_current_orientation = $orientation;
+        }
+    }
+
+    function _endPage()
+    {
+        /* End of page contents */
+        $this->_state = 1;
+    }
+
+    function _newobj()
+    {
+        /* Begin a new object */
+        $this->_n++;
+        $this->_offsets[$this->_n] = $this->_buflen + strlen($this->_buffer);
+        $this->_out($this->_n . ' 0 obj');
+    }
+
+    function _doUnderline($x, $y, $text)
+    {
+        /* Set the rectangle width according to text width. */
+        $width  = $this->getStringWidth($text, true);
+
+        /* Set rectangle position and height, using underline position and
+         * thickness settings scaled by the font size. */
+        $y = $y + ($this->_current_font['up'] * $this->_font_size_pt / 1000);
+        $height = -$this->_current_font['ut'] * $this->_font_size_pt / 1000;
+
+        return sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re f', $x, $y, $width, $height);
+    }
+
+    function _parseJPG($file)
+    {
+        /* Extract info from a JPEG file. */
+        $img = @getimagesize($file);
+        if (!$img) {
+            return $this->raiseError(sprintf('Missing or incorrect image file: %s', $file));
+        }
+        if ($img[2] != 2) {
+            return $this->raiseError(sprintf('Not a JPEG file: %s', $file));
+        }
+        if (!isset($img['channels']) || $img['channels'] == 3) {
+            $colspace = 'DeviceRGB';
+        } elseif ($img['channels'] == 4) {
+            $colspace = 'DeviceCMYK';
+        } else {
+            $colspace = 'DeviceGray';
+        }
+        $bpc = isset($img['bits']) ? $img['bits'] : 8;
+
+        /* Read whole file. */
+        $f = fopen($file, 'rb');
+        $data = fread($f, filesize($file));
+        fclose($f);
+
+        return array('w' => $img[0], 'h' => $img[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data);
+    }
+
+    function _parsePNG($file)
+    {
+        /* Extract info from a PNG file. */
+        $f = fopen($file, 'rb');
+        if (!$f) {
+            return $this->raiseError(sprintf('Unable to open image file: %s', $file));
+        }
+
+        /* Check signature. */
+        if (fread($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
+            return $this->raiseError(sprintf('Not a PNG file: %s', $file));
+        }
+
+        /* Read header chunk. */
+        fread($f, 4);
+        if (fread($f, 4) != 'IHDR') {
+            return $this->raiseError(sprintf('Incorrect PNG file: %s', $file));
+        }
+        $width = $this->_freadInt($f);
+        $height = $this->_freadInt($f);
+        $bpc = ord(fread($f, 1));
+        if ($bpc > 8) {
+            return $this->raiseError(sprintf('16-bit depth not supported: %s', $file));
+        }
+        $ct = ord(fread($f, 1));
+        if ($ct == 0) {
+            $colspace = 'DeviceGray';
+        } elseif ($ct == 2) {
+            $colspace = 'DeviceRGB';
+        } elseif ($ct == 3) {
+            $colspace = 'Indexed';
+        } else {
+            return $this->raiseError(sprintf('Alpha channel not supported: %s', $file));
+        }
+        if (ord(fread($f, 1)) != 0) {
+            return $this->raiseError(sprintf('Unknown compression method: %s', $file));
+        }
+        if (ord(fread($f, 1)) != 0) {
+            return $this->raiseError(sprintf('Unknown filter method: %s', $file));
+        }
+        if (ord(fread($f, 1)) != 0) {
+            return $this->raiseError(sprintf('Interlacing not supported: %s', $file));
+        }
+        fread($f, 4);
+        $parms = '/DecodeParms <</Predictor 15 /Colors ' . ($ct == 2 ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $width . '>>';
+        /* Scan chunks looking for palette, transparency and image data. */
+        $pal = '';
+        $trns = '';
+        $data = '';
+        do {
+            $n = $this->_freadInt($f);
+            $type = fread($f, 4);
+            if ($type == 'PLTE') {
+                /* Read palette */
+                $pal = fread($f, $n);
+                fread($f, 4);
+            } elseif ($type == 'tRNS') {
+                /* Read transparency info */
+                $t = fread($f, $n);
+                if ($ct == 0) {
+                    $trns = array(ord(substr($t, 1, 1)));
+                } elseif ($ct == 2) {
+                    $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1)));
+                } else {
+                    $pos = strpos($t, chr(0));
+                    if (is_int($pos)) {
+                        $trns = array($pos);
+                    }
+                }
+                fread($f, 4);
+            } elseif ($type == 'IDAT') {
+                /* Read image data block */
+                $data .= fread($f, $n);
+                fread($f, 4);
+            } elseif ($type == 'IEND') {
+                break;
+            } else {
+                fread($f, $n + 4);
+            }
+        } while ($n);
+
+        if ($colspace == 'Indexed' && empty($pal)) {
+            return $this->raiseError(sprintf('Missing palette in: %s', $file));
+        }
+        fclose($f);
+
+        return array('w' => $width, 'h' => $height, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $data);
+    }
+
+    function _freadInt($f)
+    {
+        /* Read a 4-byte integer from file. */
+        $i  = ord(fread($f, 1)) << 24;
+        $i += ord(fread($f, 1)) << 16;
+        $i += ord(fread($f, 1)) << 8;
+        $i += ord(fread($f, 1));
+        return $i;
+    }
+
+    function _textString($s)
+    {
+        /* Format a text string */
+        return '(' . $this->_escape($s) . ')';
+    }
+
+    function _escape($s)
+    {
+        /* Add \ before \, ( and ) */
+        return str_replace(array('\\', ')', '('),
+                           array('\\\\', '\\)', '\\('),
+                           $s);
+    }
+
+    function _putStream($s)
+    {
+        $this->_out('stream');
+        $this->_out($s);
+        $this->_out('endstream');
+    }
+
+    function _out($s)
+    {
+        /* Add a line to the document. */
+        if ($this->_state == 2) {
+            $this->_pages[$this->_page] .= $s . "\n";
+        } else {
+            $this->_buffer .= $s . "\n";
+        }
+    }
+
+}
diff --git a/framework/File_PDF/PDF/fonts/courier.php b/framework/File_PDF/PDF/fonts/courier.php
new file mode 100644 (file)
index 0000000..7223e61
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+/**
+ * @package File_PDF
+ */
+for ($i = 0; $i <= 255; $i++) {
+    $font_widths['courier'][chr($i)] = 600;
+}
+$font_widths['courierB'] = $font_widths['courier'];
+$font_widths['courierI'] = $font_widths['courier'];
+$font_widths['courierBI'] = $font_widths['courier'];
diff --git a/framework/File_PDF/PDF/fonts/helvetica.php b/framework/File_PDF/PDF/fonts/helvetica.php
new file mode 100644 (file)
index 0000000..727d4d6
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['helvetica'] = array(
+    chr(0) => 278,
+    chr(1) => 278,
+    chr(2) => 278,
+    chr(3) => 278,
+    chr(4) => 278,
+    chr(5) => 278,
+    chr(6) => 278,
+    chr(7) => 278,
+    chr(8) => 278,
+    chr(9) => 278,
+    chr(10) => 278,
+    chr(11) => 278,
+    chr(12) => 278,
+    chr(13) => 278,
+    chr(14) => 278,
+    chr(15) => 278,
+    chr(16) => 278,
+    chr(17) => 278,
+    chr(18) => 278,
+    chr(19) => 278,
+    chr(20) => 278,
+    chr(21) => 278,
+    
+    chr(22) => 278,
+    chr(23) => 278,
+    chr(24) => 278,
+    chr(25) => 278,
+    chr(26) => 278,
+    chr(27) => 278,
+    chr(28) => 278,
+    chr(29) => 278,
+    chr(30) => 278,
+    chr(31) => 278,
+    ' ' => 278,
+    '!' => 278,
+    '"' => 355,
+    '#' => 556,
+    '$' => 556,
+    '%' => 889,
+    '&' => 667,
+    '\'' => 191,
+    '(' => 333,
+    ')' => 333,
+    '*' => 389,
+    '+' => 584,
+    
+    ',' => 278,
+    '-' => 333,
+    '.' => 278,
+    '/' => 278,
+    '0' => 556,
+    '1' => 556,
+    '2' => 556,
+    '3' => 556,
+    '4' => 556,
+    '5' => 556,
+    '6' => 556,
+    '7' => 556,
+    '8' => 556,
+    '9' => 556,
+    ':' => 278,
+    ';' => 278,
+    '<' => 584,
+    '=' => 584,
+    '>' => 584,
+    '?' => 556,
+    '@' => 1015,
+    'A' => 667,
+    
+    'B' => 667,
+    'C' => 722,
+    'D' => 722,
+    'E' => 667,
+    'F' => 611,
+    'G' => 778,
+    'H' => 722,
+    'I' => 278,
+    'J' => 500,
+    'K' => 667,
+    'L' => 556,
+    'M' => 833,
+    'N' => 722,
+    'O' => 778,
+    'P' => 667,
+    'Q' => 778,
+    'R' => 722,
+    'S' => 667,
+    'T' => 611,
+    'U' => 722,
+    'V' => 667,
+    'W' => 944,
+    
+    'X' => 667,
+    'Y' => 667,
+    'Z' => 611,
+    '[' => 278,
+    '\\' => 278,
+    ']' => 278,
+    '^' => 469,
+    '_' => 556,
+    '`' => 333,
+    'a' => 556,
+    'b' => 556,
+    'c' => 500,
+    'd' => 556,
+    'e' => 556,
+    'f' => 278,
+    'g' => 556,
+    'h' => 556,
+    'i' => 222,
+    'j' => 222,
+    'k' => 500,
+    'l' => 222,
+    'm' => 833,
+    
+    'n' => 556,
+    'o' => 556,
+    'p' => 556,
+    'q' => 556,
+    'r' => 333,
+    's' => 500,
+    't' => 278,
+    'u' => 556,
+    'v' => 500,
+    'w' => 722,
+    'x' => 500,
+    'y' => 500,
+    'z' => 500,
+    '{' => 334,
+    '|' => 260,
+    '}' => 334,
+    '~' => 584,
+    chr(127) => 350,
+    chr(128) => 556,
+    chr(129) => 350,
+    chr(130) => 222,
+    chr(131) => 556,
+    
+    chr(132) => 333,
+    chr(133) => 1000,
+    chr(134) => 556,
+    chr(135) => 556,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 667,
+    chr(139) => 333,
+    chr(140) => 1000,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 222,
+    chr(146) => 222,
+    chr(147) => 333,
+    chr(148) => 333,
+    chr(149) => 350,
+    chr(150) => 556,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 500,
+    chr(155) => 333,
+    chr(156) => 944,
+    chr(157) => 350,
+    chr(158) => 500,
+    chr(159) => 667,
+    chr(160) => 278,
+    chr(161) => 333,
+    chr(162) => 556,
+    chr(163) => 556,
+    chr(164) => 556,
+    chr(165) => 556,
+    chr(166) => 260,
+    chr(167) => 556,
+    chr(168) => 333,
+    chr(169) => 737,
+    chr(170) => 370,
+    chr(171) => 556,
+    chr(172) => 584,
+    chr(173) => 333,
+    chr(174) => 737,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 584,
+    chr(178) => 333,
+    chr(179) => 333,
+    chr(180) => 333,
+    chr(181) => 556,
+    chr(182) => 537,
+    chr(183) => 278,
+    chr(184) => 333,
+    chr(185) => 333,
+    chr(186) => 365,
+    chr(187) => 556,
+    chr(188) => 834,
+    chr(189) => 834,
+    chr(190) => 834,
+    chr(191) => 611,
+    chr(192) => 667,
+    chr(193) => 667,
+    chr(194) => 667,
+    chr(195) => 667,
+    chr(196) => 667,
+    chr(197) => 667,
+    
+    chr(198) => 1000,
+    chr(199) => 722,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 278,
+    chr(205) => 278,
+    chr(206) => 278,
+    chr(207) => 278,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 778,
+    chr(211) => 778,
+    chr(212) => 778,
+    chr(213) => 778,
+    chr(214) => 778,
+    chr(215) => 584,
+    chr(216) => 778,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 667,
+    chr(222) => 667,
+    chr(223) => 611,
+    chr(224) => 556,
+    chr(225) => 556,
+    chr(226) => 556,
+    chr(227) => 556,
+    chr(228) => 556,
+    chr(229) => 556,
+    chr(230) => 889,
+    chr(231) => 500,
+    chr(232) => 556,
+    chr(233) => 556,
+    chr(234) => 556,
+    chr(235) => 556,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 556,
+    chr(241) => 556,
+    
+    chr(242) => 556,
+    chr(243) => 556,
+    chr(244) => 556,
+    chr(245) => 556,
+    chr(246) => 556,
+    chr(247) => 584,
+    chr(248) => 611,
+    chr(249) => 556,
+    chr(250) => 556,
+    chr(251) => 556,
+    chr(252) => 556,
+    chr(253) => 500,
+    chr(254) => 556,
+    chr(255) => 500);
diff --git a/framework/File_PDF/PDF/fonts/helveticab.php b/framework/File_PDF/PDF/fonts/helveticab.php
new file mode 100644 (file)
index 0000000..8bed545
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['helveticaB'] = array(
+    chr(0) => 278,
+    chr(1) => 278,
+    chr(2) => 278,
+    chr(3) => 278,
+    chr(4) => 278,
+    chr(5) => 278,
+    chr(6) => 278,
+    chr(7) => 278,
+    chr(8) => 278,
+    chr(9) => 278,
+    chr(10) => 278,
+    chr(11) => 278,
+    chr(12) => 278,
+    chr(13) => 278,
+    chr(14) => 278,
+    chr(15) => 278,
+    chr(16) => 278,
+    chr(17) => 278,
+    chr(18) => 278,
+    chr(19) => 278,
+    chr(20) => 278,
+    chr(21) => 278,
+    
+    chr(22) => 278,
+    chr(23) => 278,
+    chr(24) => 278,
+    chr(25) => 278,
+    chr(26) => 278,
+    chr(27) => 278,
+    chr(28) => 278,
+    chr(29) => 278,
+    chr(30) => 278,
+    chr(31) => 278,
+    ' ' => 278,
+    '!' => 333,
+    '"' => 474,
+    '#' => 556,
+    '$' => 556,
+    '%' => 889,
+    '&' => 722,
+    '\'' => 238,
+    '(' => 333,
+    ')' => 333,
+    '*' => 389,
+    '+' => 584,
+    
+    ',' => 278,
+    '-' => 333,
+    '.' => 278,
+    '/' => 278,
+    '0' => 556,
+    '1' => 556,
+    '2' => 556,
+    '3' => 556,
+    '4' => 556,
+    '5' => 556,
+    '6' => 556,
+    '7' => 556,
+    '8' => 556,
+    '9' => 556,
+    ':' => 333,
+    ';' => 333,
+    '<' => 584,
+    '=' => 584,
+    '>' => 584,
+    '?' => 611,
+    '@' => 975,
+    'A' => 722,
+    
+    'B' => 722,
+    'C' => 722,
+    'D' => 722,
+    'E' => 667,
+    'F' => 611,
+    'G' => 778,
+    'H' => 722,
+    'I' => 278,
+    'J' => 556,
+    'K' => 722,
+    'L' => 611,
+    'M' => 833,
+    'N' => 722,
+    'O' => 778,
+    'P' => 667,
+    'Q' => 778,
+    'R' => 722,
+    'S' => 667,
+    'T' => 611,
+    'U' => 722,
+    'V' => 667,
+    'W' => 944,
+    
+    'X' => 667,
+    'Y' => 667,
+    'Z' => 611,
+    '[' => 333,
+    '\\' => 278,
+    ']' => 333,
+    '^' => 584,
+    '_' => 556,
+    '`' => 333,
+    'a' => 556,
+    'b' => 611,
+    'c' => 556,
+    'd' => 611,
+    'e' => 556,
+    'f' => 333,
+    'g' => 611,
+    'h' => 611,
+    'i' => 278,
+    'j' => 278,
+    'k' => 556,
+    'l' => 278,
+    'm' => 889,
+    
+    'n' => 611,
+    'o' => 611,
+    'p' => 611,
+    'q' => 611,
+    'r' => 389,
+    's' => 556,
+    't' => 333,
+    'u' => 611,
+    'v' => 556,
+    'w' => 778,
+    'x' => 556,
+    'y' => 556,
+    'z' => 500,
+    '{' => 389,
+    '|' => 280,
+    '}' => 389,
+    '~' => 584,
+    chr(127) => 350,
+    chr(128) => 556,
+    chr(129) => 350,
+    chr(130) => 278,
+    chr(131) => 556,
+    
+    chr(132) => 500,
+    chr(133) => 1000,
+    chr(134) => 556,
+    chr(135) => 556,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 667,
+    chr(139) => 333,
+    chr(140) => 1000,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 278,
+    chr(146) => 278,
+    chr(147) => 500,
+    chr(148) => 500,
+    chr(149) => 350,
+    chr(150) => 556,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 556,
+    chr(155) => 333,
+    chr(156) => 944,
+    chr(157) => 350,
+    chr(158) => 500,
+    chr(159) => 667,
+    chr(160) => 278,
+    chr(161) => 333,
+    chr(162) => 556,
+    chr(163) => 556,
+    chr(164) => 556,
+    chr(165) => 556,
+    chr(166) => 280,
+    chr(167) => 556,
+    chr(168) => 333,
+    chr(169) => 737,
+    chr(170) => 370,
+    chr(171) => 556,
+    chr(172) => 584,
+    chr(173) => 333,
+    chr(174) => 737,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 584,
+    chr(178) => 333,
+    chr(179) => 333,
+    chr(180) => 333,
+    chr(181) => 611,
+    chr(182) => 556,
+    chr(183) => 278,
+    chr(184) => 333,
+    chr(185) => 333,
+    chr(186) => 365,
+    chr(187) => 556,
+    chr(188) => 834,
+    chr(189) => 834,
+    chr(190) => 834,
+    chr(191) => 611,
+    chr(192) => 722,
+    chr(193) => 722,
+    chr(194) => 722,
+    chr(195) => 722,
+    chr(196) => 722,
+    chr(197) => 722,
+    
+    chr(198) => 1000,
+    chr(199) => 722,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 278,
+    chr(205) => 278,
+    chr(206) => 278,
+    chr(207) => 278,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 778,
+    chr(211) => 778,
+    chr(212) => 778,
+    chr(213) => 778,
+    chr(214) => 778,
+    chr(215) => 584,
+    chr(216) => 778,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 667,
+    chr(222) => 667,
+    chr(223) => 611,
+    chr(224) => 556,
+    chr(225) => 556,
+    chr(226) => 556,
+    chr(227) => 556,
+    chr(228) => 556,
+    chr(229) => 556,
+    chr(230) => 889,
+    chr(231) => 556,
+    chr(232) => 556,
+    chr(233) => 556,
+    chr(234) => 556,
+    chr(235) => 556,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 611,
+    chr(241) => 611,
+    
+    chr(242) => 611,
+    chr(243) => 611,
+    chr(244) => 611,
+    chr(245) => 611,
+    chr(246) => 611,
+    chr(247) => 584,
+    chr(248) => 611,
+    chr(249) => 611,
+    chr(250) => 611,
+    chr(251) => 611,
+    chr(252) => 611,
+    chr(253) => 556,
+    chr(254) => 611,
+    chr(255) => 556);
diff --git a/framework/File_PDF/PDF/fonts/helveticabi.php b/framework/File_PDF/PDF/fonts/helveticabi.php
new file mode 100644 (file)
index 0000000..495d087
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['helveticaBI'] = array(
+    chr(0) => 278,
+    chr(1) => 278,
+    chr(2) => 278,
+    chr(3) => 278,
+    chr(4) => 278,
+    chr(5) => 278,
+    chr(6) => 278,
+    chr(7) => 278,
+    chr(8) => 278,
+    chr(9) => 278,
+    chr(10) => 278,
+    chr(11) => 278,
+    chr(12) => 278,
+    chr(13) => 278,
+    chr(14) => 278,
+    chr(15) => 278,
+    chr(16) => 278,
+    chr(17) => 278,
+    chr(18) => 278,
+    chr(19) => 278,
+    chr(20) => 278,
+    chr(21) => 278,
+    
+    chr(22) => 278,
+    chr(23) => 278,
+    chr(24) => 278,
+    chr(25) => 278,
+    chr(26) => 278,
+    chr(27) => 278,
+    chr(28) => 278,
+    chr(29) => 278,
+    chr(30) => 278,
+    chr(31) => 278,
+    ' ' => 278,
+    '!' => 333,
+    '"' => 474,
+    '#' => 556,
+    '$' => 556,
+    '%' => 889,
+    '&' => 722,
+    '\'' => 238,
+    '(' => 333,
+    ')' => 333,
+    '*' => 389,
+    '+' => 584,
+    
+    ',' => 278,
+    '-' => 333,
+    '.' => 278,
+    '/' => 278,
+    '0' => 556,
+    '1' => 556,
+    '2' => 556,
+    '3' => 556,
+    '4' => 556,
+    '5' => 556,
+    '6' => 556,
+    '7' => 556,
+    '8' => 556,
+    '9' => 556,
+    ':' => 333,
+    ';' => 333,
+    '<' => 584,
+    '=' => 584,
+    '>' => 584,
+    '?' => 611,
+    '@' => 975,
+    'A' => 722,
+    
+    'B' => 722,
+    'C' => 722,
+    'D' => 722,
+    'E' => 667,
+    'F' => 611,
+    'G' => 778,
+    'H' => 722,
+    'I' => 278,
+    'J' => 556,
+    'K' => 722,
+    'L' => 611,
+    'M' => 833,
+    'N' => 722,
+    'O' => 778,
+    'P' => 667,
+    'Q' => 778,
+    'R' => 722,
+    'S' => 667,
+    'T' => 611,
+    'U' => 722,
+    'V' => 667,
+    'W' => 944,
+    
+    'X' => 667,
+    'Y' => 667,
+    'Z' => 611,
+    '[' => 333,
+    '\\' => 278,
+    ']' => 333,
+    '^' => 584,
+    '_' => 556,
+    '`' => 333,
+    'a' => 556,
+    'b' => 611,
+    'c' => 556,
+    'd' => 611,
+    'e' => 556,
+    'f' => 333,
+    'g' => 611,
+    'h' => 611,
+    'i' => 278,
+    'j' => 278,
+    'k' => 556,
+    'l' => 278,
+    'm' => 889,
+    
+    'n' => 611,
+    'o' => 611,
+    'p' => 611,
+    'q' => 611,
+    'r' => 389,
+    's' => 556,
+    't' => 333,
+    'u' => 611,
+    'v' => 556,
+    'w' => 778,
+    'x' => 556,
+    'y' => 556,
+    'z' => 500,
+    '{' => 389,
+    '|' => 280,
+    '}' => 389,
+    '~' => 584,
+    chr(127) => 350,
+    chr(128) => 556,
+    chr(129) => 350,
+    chr(130) => 278,
+    chr(131) => 556,
+    
+    chr(132) => 500,
+    chr(133) => 1000,
+    chr(134) => 556,
+    chr(135) => 556,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 667,
+    chr(139) => 333,
+    chr(140) => 1000,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 278,
+    chr(146) => 278,
+    chr(147) => 500,
+    chr(148) => 500,
+    chr(149) => 350,
+    chr(150) => 556,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 556,
+    chr(155) => 333,
+    chr(156) => 944,
+    chr(157) => 350,
+    chr(158) => 500,
+    chr(159) => 667,
+    chr(160) => 278,
+    chr(161) => 333,
+    chr(162) => 556,
+    chr(163) => 556,
+    chr(164) => 556,
+    chr(165) => 556,
+    chr(166) => 280,
+    chr(167) => 556,
+    chr(168) => 333,
+    chr(169) => 737,
+    chr(170) => 370,
+    chr(171) => 556,
+    chr(172) => 584,
+    chr(173) => 333,
+    chr(174) => 737,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 584,
+    chr(178) => 333,
+    chr(179) => 333,
+    chr(180) => 333,
+    chr(181) => 611,
+    chr(182) => 556,
+    chr(183) => 278,
+    chr(184) => 333,
+    chr(185) => 333,
+    chr(186) => 365,
+    chr(187) => 556,
+    chr(188) => 834,
+    chr(189) => 834,
+    chr(190) => 834,
+    chr(191) => 611,
+    chr(192) => 722,
+    chr(193) => 722,
+    chr(194) => 722,
+    chr(195) => 722,
+    chr(196) => 722,
+    chr(197) => 722,
+    
+    chr(198) => 1000,
+    chr(199) => 722,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 278,
+    chr(205) => 278,
+    chr(206) => 278,
+    chr(207) => 278,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 778,
+    chr(211) => 778,
+    chr(212) => 778,
+    chr(213) => 778,
+    chr(214) => 778,
+    chr(215) => 584,
+    chr(216) => 778,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 667,
+    chr(222) => 667,
+    chr(223) => 611,
+    chr(224) => 556,
+    chr(225) => 556,
+    chr(226) => 556,
+    chr(227) => 556,
+    chr(228) => 556,
+    chr(229) => 556,
+    chr(230) => 889,
+    chr(231) => 556,
+    chr(232) => 556,
+    chr(233) => 556,
+    chr(234) => 556,
+    chr(235) => 556,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 611,
+    chr(241) => 611,
+    
+    chr(242) => 611,
+    chr(243) => 611,
+    chr(244) => 611,
+    chr(245) => 611,
+    chr(246) => 611,
+    chr(247) => 584,
+    chr(248) => 611,
+    chr(249) => 611,
+    chr(250) => 611,
+    chr(251) => 611,
+    chr(252) => 611,
+    chr(253) => 556,
+    chr(254) => 611,
+    chr(255) => 556);
diff --git a/framework/File_PDF/PDF/fonts/helveticai.php b/framework/File_PDF/PDF/fonts/helveticai.php
new file mode 100644 (file)
index 0000000..681c8e8
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['helveticaI'] = array(
+    chr(0) => 278,
+    chr(1) => 278,
+    chr(2) => 278,
+    chr(3) => 278,
+    chr(4) => 278,
+    chr(5) => 278,
+    chr(6) => 278,
+    chr(7) => 278,
+    chr(8) => 278,
+    chr(9) => 278,
+    chr(10) => 278,
+    chr(11) => 278,
+    chr(12) => 278,
+    chr(13) => 278,
+    chr(14) => 278,
+    chr(15) => 278,
+    chr(16) => 278,
+    chr(17) => 278,
+    chr(18) => 278,
+    chr(19) => 278,
+    chr(20) => 278,
+    chr(21) => 278,
+    
+    chr(22) => 278,
+    chr(23) => 278,
+    chr(24) => 278,
+    chr(25) => 278,
+    chr(26) => 278,
+    chr(27) => 278,
+    chr(28) => 278,
+    chr(29) => 278,
+    chr(30) => 278,
+    chr(31) => 278,
+    ' ' => 278,
+    '!' => 278,
+    '"' => 355,
+    '#' => 556,
+    '$' => 556,
+    '%' => 889,
+    '&' => 667,
+    '\'' => 191,
+    '(' => 333,
+    ')' => 333,
+    '*' => 389,
+    '+' => 584,
+    
+    ',' => 278,
+    '-' => 333,
+    '.' => 278,
+    '/' => 278,
+    '0' => 556,
+    '1' => 556,
+    '2' => 556,
+    '3' => 556,
+    '4' => 556,
+    '5' => 556,
+    '6' => 556,
+    '7' => 556,
+    '8' => 556,
+    '9' => 556,
+    ':' => 278,
+    ';' => 278,
+    '<' => 584,
+    '=' => 584,
+    '>' => 584,
+    '?' => 556,
+    '@' => 1015,
+    'A' => 667,
+    
+    'B' => 667,
+    'C' => 722,
+    'D' => 722,
+    'E' => 667,
+    'F' => 611,
+    'G' => 778,
+    'H' => 722,
+    'I' => 278,
+    'J' => 500,
+    'K' => 667,
+    'L' => 556,
+    'M' => 833,
+    'N' => 722,
+    'O' => 778,
+    'P' => 667,
+    'Q' => 778,
+    'R' => 722,
+    'S' => 667,
+    'T' => 611,
+    'U' => 722,
+    'V' => 667,
+    'W' => 944,
+    
+    'X' => 667,
+    'Y' => 667,
+    'Z' => 611,
+    '[' => 278,
+    '\\' => 278,
+    ']' => 278,
+    '^' => 469,
+    '_' => 556,
+    '`' => 333,
+    'a' => 556,
+    'b' => 556,
+    'c' => 500,
+    'd' => 556,
+    'e' => 556,
+    'f' => 278,
+    'g' => 556,
+    'h' => 556,
+    'i' => 222,
+    'j' => 222,
+    'k' => 500,
+    'l' => 222,
+    'm' => 833,
+    
+    'n' => 556,
+    'o' => 556,
+    'p' => 556,
+    'q' => 556,
+    'r' => 333,
+    's' => 500,
+    't' => 278,
+    'u' => 556,
+    'v' => 500,
+    'w' => 722,
+    'x' => 500,
+    'y' => 500,
+    'z' => 500,
+    '{' => 334,
+    '|' => 260,
+    '}' => 334,
+    '~' => 584,
+    chr(127) => 350,
+    chr(128) => 556,
+    chr(129) => 350,
+    chr(130) => 222,
+    chr(131) => 556,
+    
+    chr(132) => 333,
+    chr(133) => 1000,
+    chr(134) => 556,
+    chr(135) => 556,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 667,
+    chr(139) => 333,
+    chr(140) => 1000,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 222,
+    chr(146) => 222,
+    chr(147) => 333,
+    chr(148) => 333,
+    chr(149) => 350,
+    chr(150) => 556,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 500,
+    chr(155) => 333,
+    chr(156) => 944,
+    chr(157) => 350,
+    chr(158) => 500,
+    chr(159) => 667,
+    chr(160) => 278,
+    chr(161) => 333,
+    chr(162) => 556,
+    chr(163) => 556,
+    chr(164) => 556,
+    chr(165) => 556,
+    chr(166) => 260,
+    chr(167) => 556,
+    chr(168) => 333,
+    chr(169) => 737,
+    chr(170) => 370,
+    chr(171) => 556,
+    chr(172) => 584,
+    chr(173) => 333,
+    chr(174) => 737,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 584,
+    chr(178) => 333,
+    chr(179) => 333,
+    chr(180) => 333,
+    chr(181) => 556,
+    chr(182) => 537,
+    chr(183) => 278,
+    chr(184) => 333,
+    chr(185) => 333,
+    chr(186) => 365,
+    chr(187) => 556,
+    chr(188) => 834,
+    chr(189) => 834,
+    chr(190) => 834,
+    chr(191) => 611,
+    chr(192) => 667,
+    chr(193) => 667,
+    chr(194) => 667,
+    chr(195) => 667,
+    chr(196) => 667,
+    chr(197) => 667,
+    
+    chr(198) => 1000,
+    chr(199) => 722,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 278,
+    chr(205) => 278,
+    chr(206) => 278,
+    chr(207) => 278,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 778,
+    chr(211) => 778,
+    chr(212) => 778,
+    chr(213) => 778,
+    chr(214) => 778,
+    chr(215) => 584,
+    chr(216) => 778,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 667,
+    chr(222) => 667,
+    chr(223) => 611,
+    chr(224) => 556,
+    chr(225) => 556,
+    chr(226) => 556,
+    chr(227) => 556,
+    chr(228) => 556,
+    chr(229) => 556,
+    chr(230) => 889,
+    chr(231) => 500,
+    chr(232) => 556,
+    chr(233) => 556,
+    chr(234) => 556,
+    chr(235) => 556,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 556,
+    chr(241) => 556,
+    
+    chr(242) => 556,
+    chr(243) => 556,
+    chr(244) => 556,
+    chr(245) => 556,
+    chr(246) => 556,
+    chr(247) => 584,
+    chr(248) => 611,
+    chr(249) => 556,
+    chr(250) => 556,
+    chr(251) => 556,
+    chr(252) => 556,
+    chr(253) => 500,
+    chr(254) => 556,
+    chr(255) => 500);
diff --git a/framework/File_PDF/PDF/fonts/symbol.php b/framework/File_PDF/PDF/fonts/symbol.php
new file mode 100644 (file)
index 0000000..dd1f660
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['symbol'] = array(
+    chr(0) => 250,
+    chr(1) => 250,
+    chr(2) => 250,
+    chr(3) => 250,
+    chr(4) => 250,
+    chr(5) => 250,
+    chr(6) => 250,
+    chr(7) => 250,
+    chr(8) => 250,
+    chr(9) => 250,
+    chr(10) => 250,
+    chr(11) => 250,
+    chr(12) => 250,
+    chr(13) => 250,
+    chr(14) => 250,
+    chr(15) => 250,
+    chr(16) => 250,
+    chr(17) => 250,
+    chr(18) => 250,
+    chr(19) => 250,
+    chr(20) => 250,
+    chr(21) => 250,
+    
+    chr(22) => 250,
+    chr(23) => 250,
+    chr(24) => 250,
+    chr(25) => 250,
+    chr(26) => 250,
+    chr(27) => 250,
+    chr(28) => 250,
+    chr(29) => 250,
+    chr(30) => 250,
+    chr(31) => 250,
+    ' ' => 250,
+    '!' => 333,
+    '"' => 713,
+    '#' => 500,
+    '$' => 549,
+    '%' => 833,
+    '&' => 778,
+    '\'' => 439,
+    '(' => 333,
+    ')' => 333,
+    '*' => 500,
+    '+' => 549,
+    
+    ',' => 250,
+    '-' => 549,
+    '.' => 250,
+    '/' => 278,
+    '0' => 500,
+    '1' => 500,
+    '2' => 500,
+    '3' => 500,
+    '4' => 500,
+    '5' => 500,
+    '6' => 500,
+    '7' => 500,
+    '8' => 500,
+    '9' => 500,
+    ':' => 278,
+    ';' => 278,
+    '<' => 549,
+    '=' => 549,
+    '>' => 549,
+    '?' => 444,
+    '@' => 549,
+    'A' => 722,
+    
+    'B' => 667,
+    'C' => 722,
+    'D' => 612,
+    'E' => 611,
+    'F' => 763,
+    'G' => 603,
+    'H' => 722,
+    'I' => 333,
+    'J' => 631,
+    'K' => 722,
+    'L' => 686,
+    'M' => 889,
+    'N' => 722,
+    'O' => 722,
+    'P' => 768,
+    'Q' => 741,
+    'R' => 556,
+    'S' => 592,
+    'T' => 611,
+    'U' => 690,
+    'V' => 439,
+    'W' => 768,
+    
+    'X' => 645,
+    'Y' => 795,
+    'Z' => 611,
+    '[' => 333,
+    '\\' => 863,
+    ']' => 333,
+    '^' => 658,
+    '_' => 500,
+    '`' => 500,
+    'a' => 631,
+    'b' => 549,
+    'c' => 549,
+    'd' => 494,
+    'e' => 439,
+    'f' => 521,
+    'g' => 411,
+    'h' => 603,
+    'i' => 329,
+    'j' => 603,
+    'k' => 549,
+    'l' => 549,
+    'm' => 576,
+    
+    'n' => 521,
+    'o' => 549,
+    'p' => 549,
+    'q' => 521,
+    'r' => 549,
+    's' => 603,
+    't' => 439,
+    'u' => 576,
+    'v' => 713,
+    'w' => 686,
+    'x' => 493,
+    'y' => 686,
+    'z' => 494,
+    '{' => 480,
+    '|' => 200,
+    '}' => 480,
+    '~' => 549,
+    chr(127) => 0,
+    chr(128) => 0,
+    chr(129) => 0,
+    chr(130) => 0,
+    chr(131) => 0,
+    
+    chr(132) => 0,
+    chr(133) => 0,
+    chr(134) => 0,
+    chr(135) => 0,
+    chr(136) => 0,
+    chr(137) => 0,
+    chr(138) => 0,
+    chr(139) => 0,
+    chr(140) => 0,
+    chr(141) => 0,
+    chr(142) => 0,
+    chr(143) => 0,
+    chr(144) => 0,
+    chr(145) => 0,
+    chr(146) => 0,
+    chr(147) => 0,
+    chr(148) => 0,
+    chr(149) => 0,
+    chr(150) => 0,
+    chr(151) => 0,
+    chr(152) => 0,
+    chr(153) => 0,
+    
+    chr(154) => 0,
+    chr(155) => 0,
+    chr(156) => 0,
+    chr(157) => 0,
+    chr(158) => 0,
+    chr(159) => 0,
+    chr(160) => 750,
+    chr(161) => 620,
+    chr(162) => 247,
+    chr(163) => 549,
+    chr(164) => 167,
+    chr(165) => 713,
+    chr(166) => 500,
+    chr(167) => 753,
+    chr(168) => 753,
+    chr(169) => 753,
+    chr(170) => 753,
+    chr(171) => 1042,
+    chr(172) => 987,
+    chr(173) => 603,
+    chr(174) => 987,
+    chr(175) => 603,
+    
+    chr(176) => 400,
+    chr(177) => 549,
+    chr(178) => 411,
+    chr(179) => 549,
+    chr(180) => 549,
+    chr(181) => 713,
+    chr(182) => 494,
+    chr(183) => 460,
+    chr(184) => 549,
+    chr(185) => 549,
+    chr(186) => 549,
+    chr(187) => 549,
+    chr(188) => 1000,
+    chr(189) => 603,
+    chr(190) => 1000,
+    chr(191) => 658,
+    chr(192) => 823,
+    chr(193) => 686,
+    chr(194) => 795,
+    chr(195) => 987,
+    chr(196) => 768,
+    chr(197) => 768,
+    
+    chr(198) => 823,
+    chr(199) => 768,
+    chr(200) => 768,
+    chr(201) => 713,
+    chr(202) => 713,
+    chr(203) => 713,
+    chr(204) => 713,
+    chr(205) => 713,
+    chr(206) => 713,
+    chr(207) => 713,
+    chr(208) => 768,
+    chr(209) => 713,
+    chr(210) => 790,
+    chr(211) => 790,
+    chr(212) => 890,
+    chr(213) => 823,
+    chr(214) => 549,
+    chr(215) => 250,
+    chr(216) => 713,
+    chr(217) => 603,
+    chr(218) => 603,
+    chr(219) => 1042,
+    
+    chr(220) => 987,
+    chr(221) => 603,
+    chr(222) => 987,
+    chr(223) => 603,
+    chr(224) => 494,
+    chr(225) => 329,
+    chr(226) => 790,
+    chr(227) => 790,
+    chr(228) => 786,
+    chr(229) => 713,
+    chr(230) => 384,
+    chr(231) => 384,
+    chr(232) => 384,
+    chr(233) => 384,
+    chr(234) => 384,
+    chr(235) => 384,
+    chr(236) => 494,
+    chr(237) => 494,
+    chr(238) => 494,
+    chr(239) => 494,
+    chr(240) => 0,
+    chr(241) => 329,
+    
+    chr(242) => 274,
+    chr(243) => 686,
+    chr(244) => 686,
+    chr(245) => 686,
+    chr(246) => 384,
+    chr(247) => 384,
+    chr(248) => 384,
+    chr(249) => 384,
+    chr(250) => 384,
+    chr(251) => 384,
+    chr(252) => 494,
+    chr(253) => 494,
+    chr(254) => 494,
+    chr(255) => 0);
diff --git a/framework/File_PDF/PDF/fonts/times.php b/framework/File_PDF/PDF/fonts/times.php
new file mode 100644 (file)
index 0000000..28507c6
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['times'] = array(
+    chr(0) => 250,
+    chr(1) => 250,
+    chr(2) => 250,
+    chr(3) => 250,
+    chr(4) => 250,
+    chr(5) => 250,
+    chr(6) => 250,
+    chr(7) => 250,
+    chr(8) => 250,
+    chr(9) => 250,
+    chr(10) => 250,
+    chr(11) => 250,
+    chr(12) => 250,
+    chr(13) => 250,
+    chr(14) => 250,
+    chr(15) => 250,
+    chr(16) => 250,
+    chr(17) => 250,
+    chr(18) => 250,
+    chr(19) => 250,
+    chr(20) => 250,
+    chr(21) => 250,
+    
+    chr(22) => 250,
+    chr(23) => 250,
+    chr(24) => 250,
+    chr(25) => 250,
+    chr(26) => 250,
+    chr(27) => 250,
+    chr(28) => 250,
+    chr(29) => 250,
+    chr(30) => 250,
+    chr(31) => 250,
+    ' ' => 250,
+    '!' => 333,
+    '"' => 408,
+    '#' => 500,
+    '$' => 500,
+    '%' => 833,
+    '&' => 778,
+    '\'' => 180,
+    '(' => 333,
+    ')' => 333,
+    '*' => 500,
+    '+' => 564,
+    
+    ',' => 250,
+    '-' => 333,
+    '.' => 250,
+    '/' => 278,
+    '0' => 500,
+    '1' => 500,
+    '2' => 500,
+    '3' => 500,
+    '4' => 500,
+    '5' => 500,
+    '6' => 500,
+    '7' => 500,
+    '8' => 500,
+    '9' => 500,
+    ':' => 278,
+    ';' => 278,
+    '<' => 564,
+    '=' => 564,
+    '>' => 564,
+    '?' => 444,
+    '@' => 921,
+    'A' => 722,
+    
+    'B' => 667,
+    'C' => 667,
+    'D' => 722,
+    'E' => 611,
+    'F' => 556,
+    'G' => 722,
+    'H' => 722,
+    'I' => 333,
+    'J' => 389,
+    'K' => 722,
+    'L' => 611,
+    'M' => 889,
+    'N' => 722,
+    'O' => 722,
+    'P' => 556,
+    'Q' => 722,
+    'R' => 667,
+    'S' => 556,
+    'T' => 611,
+    'U' => 722,
+    'V' => 722,
+    'W' => 944,
+    
+    'X' => 722,
+    'Y' => 722,
+    'Z' => 611,
+    '[' => 333,
+    '\\' => 278,
+    ']' => 333,
+    '^' => 469,
+    '_' => 500,
+    '`' => 333,
+    'a' => 444,
+    'b' => 500,
+    'c' => 444,
+    'd' => 500,
+    'e' => 444,
+    'f' => 333,
+    'g' => 500,
+    'h' => 500,
+    'i' => 278,
+    'j' => 278,
+    'k' => 500,
+    'l' => 278,
+    'm' => 778,
+    
+    'n' => 500,
+    'o' => 500,
+    'p' => 500,
+    'q' => 500,
+    'r' => 333,
+    's' => 389,
+    't' => 278,
+    'u' => 500,
+    'v' => 500,
+    'w' => 722,
+    'x' => 500,
+    'y' => 500,
+    'z' => 444,
+    '{' => 480,
+    '|' => 200,
+    '}' => 480,
+    '~' => 541,
+    chr(127) => 350,
+    chr(128) => 500,
+    chr(129) => 350,
+    chr(130) => 333,
+    chr(131) => 500,
+    
+    chr(132) => 444,
+    chr(133) => 1000,
+    chr(134) => 500,
+    chr(135) => 500,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 556,
+    chr(139) => 333,
+    chr(140) => 889,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 333,
+    chr(146) => 333,
+    chr(147) => 444,
+    chr(148) => 444,
+    chr(149) => 350,
+    chr(150) => 500,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 980,
+    
+    chr(154) => 389,
+    chr(155) => 333,
+    chr(156) => 722,
+    chr(157) => 350,
+    chr(158) => 444,
+    chr(159) => 722,
+    chr(160) => 250,
+    chr(161) => 333,
+    chr(162) => 500,
+    chr(163) => 500,
+    chr(164) => 500,
+    chr(165) => 500,
+    chr(166) => 200,
+    chr(167) => 500,
+    chr(168) => 333,
+    chr(169) => 760,
+    chr(170) => 276,
+    chr(171) => 500,
+    chr(172) => 564,
+    chr(173) => 333,
+    chr(174) => 760,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 564,
+    chr(178) => 300,
+    chr(179) => 300,
+    chr(180) => 333,
+    chr(181) => 500,
+    chr(182) => 453,
+    chr(183) => 250,
+    chr(184) => 333,
+    chr(185) => 300,
+    chr(186) => 310,
+    chr(187) => 500,
+    chr(188) => 750,
+    chr(189) => 750,
+    chr(190) => 750,
+    chr(191) => 444,
+    chr(192) => 722,
+    chr(193) => 722,
+    chr(194) => 722,
+    chr(195) => 722,
+    chr(196) => 722,
+    chr(197) => 722,
+    
+    chr(198) => 889,
+    chr(199) => 667,
+    chr(200) => 611,
+    chr(201) => 611,
+    chr(202) => 611,
+    chr(203) => 611,
+    chr(204) => 333,
+    chr(205) => 333,
+    chr(206) => 333,
+    chr(207) => 333,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 722,
+    chr(211) => 722,
+    chr(212) => 722,
+    chr(213) => 722,
+    chr(214) => 722,
+    chr(215) => 564,
+    chr(216) => 722,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 722,
+    chr(222) => 556,
+    chr(223) => 500,
+    chr(224) => 444,
+    chr(225) => 444,
+    chr(226) => 444,
+    chr(227) => 444,
+    chr(228) => 444,
+    chr(229) => 444,
+    chr(230) => 667,
+    chr(231) => 444,
+    chr(232) => 444,
+    chr(233) => 444,
+    chr(234) => 444,
+    chr(235) => 444,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 500,
+    chr(241) => 500,
+    
+    chr(242) => 500,
+    chr(243) => 500,
+    chr(244) => 500,
+    chr(245) => 500,
+    chr(246) => 500,
+    chr(247) => 564,
+    chr(248) => 500,
+    chr(249) => 500,
+    chr(250) => 500,
+    chr(251) => 500,
+    chr(252) => 500,
+    chr(253) => 500,
+    chr(254) => 500,
+    chr(255) => 500);
diff --git a/framework/File_PDF/PDF/fonts/timesb.php b/framework/File_PDF/PDF/fonts/timesb.php
new file mode 100644 (file)
index 0000000..422923f
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['timesB'] = array(
+    chr(0) => 250,
+    chr(1) => 250,
+    chr(2) => 250,
+    chr(3) => 250,
+    chr(4) => 250,
+    chr(5) => 250,
+    chr(6) => 250,
+    chr(7) => 250,
+    chr(8) => 250,
+    chr(9) => 250,
+    chr(10) => 250,
+    chr(11) => 250,
+    chr(12) => 250,
+    chr(13) => 250,
+    chr(14) => 250,
+    chr(15) => 250,
+    chr(16) => 250,
+    chr(17) => 250,
+    chr(18) => 250,
+    chr(19) => 250,
+    chr(20) => 250,
+    chr(21) => 250,
+    
+    chr(22) => 250,
+    chr(23) => 250,
+    chr(24) => 250,
+    chr(25) => 250,
+    chr(26) => 250,
+    chr(27) => 250,
+    chr(28) => 250,
+    chr(29) => 250,
+    chr(30) => 250,
+    chr(31) => 250,
+    ' ' => 250,
+    '!' => 333,
+    '"' => 555,
+    '#' => 500,
+    '$' => 500,
+    '%' => 1000,
+    '&' => 833,
+    '\'' => 278,
+    '(' => 333,
+    ')' => 333,
+    '*' => 500,
+    '+' => 570,
+    
+    ',' => 250,
+    '-' => 333,
+    '.' => 250,
+    '/' => 278,
+    '0' => 500,
+    '1' => 500,
+    '2' => 500,
+    '3' => 500,
+    '4' => 500,
+    '5' => 500,
+    '6' => 500,
+    '7' => 500,
+    '8' => 500,
+    '9' => 500,
+    ':' => 333,
+    ';' => 333,
+    '<' => 570,
+    '=' => 570,
+    '>' => 570,
+    '?' => 500,
+    '@' => 930,
+    'A' => 722,
+    
+    'B' => 667,
+    'C' => 722,
+    'D' => 722,
+    'E' => 667,
+    'F' => 611,
+    'G' => 778,
+    'H' => 778,
+    'I' => 389,
+    'J' => 500,
+    'K' => 778,
+    'L' => 667,
+    'M' => 944,
+    'N' => 722,
+    'O' => 778,
+    'P' => 611,
+    'Q' => 778,
+    'R' => 722,
+    'S' => 556,
+    'T' => 667,
+    'U' => 722,
+    'V' => 722,
+    'W' => 1000,
+    
+    'X' => 722,
+    'Y' => 722,
+    'Z' => 667,
+    '[' => 333,
+    '\\' => 278,
+    ']' => 333,
+    '^' => 581,
+    '_' => 500,
+    '`' => 333,
+    'a' => 500,
+    'b' => 556,
+    'c' => 444,
+    'd' => 556,
+    'e' => 444,
+    'f' => 333,
+    'g' => 500,
+    'h' => 556,
+    'i' => 278,
+    'j' => 333,
+    'k' => 556,
+    'l' => 278,
+    'm' => 833,
+    
+    'n' => 556,
+    'o' => 500,
+    'p' => 556,
+    'q' => 556,
+    'r' => 444,
+    's' => 389,
+    't' => 333,
+    'u' => 556,
+    'v' => 500,
+    'w' => 722,
+    'x' => 500,
+    'y' => 500,
+    'z' => 444,
+    '{' => 394,
+    '|' => 220,
+    '}' => 394,
+    '~' => 520,
+    chr(127) => 350,
+    chr(128) => 500,
+    chr(129) => 350,
+    chr(130) => 333,
+    chr(131) => 500,
+    
+    chr(132) => 500,
+    chr(133) => 1000,
+    chr(134) => 500,
+    chr(135) => 500,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 556,
+    chr(139) => 333,
+    chr(140) => 1000,
+    chr(141) => 350,
+    chr(142) => 667,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 333,
+    chr(146) => 333,
+    chr(147) => 500,
+    chr(148) => 500,
+    chr(149) => 350,
+    chr(150) => 500,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 389,
+    chr(155) => 333,
+    chr(156) => 722,
+    chr(157) => 350,
+    chr(158) => 444,
+    chr(159) => 722,
+    chr(160) => 250,
+    chr(161) => 333,
+    chr(162) => 500,
+    chr(163) => 500,
+    chr(164) => 500,
+    chr(165) => 500,
+    chr(166) => 220,
+    chr(167) => 500,
+    chr(168) => 333,
+    chr(169) => 747,
+    chr(170) => 300,
+    chr(171) => 500,
+    chr(172) => 570,
+    chr(173) => 333,
+    chr(174) => 747,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 570,
+    chr(178) => 300,
+    chr(179) => 300,
+    chr(180) => 333,
+    chr(181) => 556,
+    chr(182) => 540,
+    chr(183) => 250,
+    chr(184) => 333,
+    chr(185) => 300,
+    chr(186) => 330,
+    chr(187) => 500,
+    chr(188) => 750,
+    chr(189) => 750,
+    chr(190) => 750,
+    chr(191) => 500,
+    chr(192) => 722,
+    chr(193) => 722,
+    chr(194) => 722,
+    chr(195) => 722,
+    chr(196) => 722,
+    chr(197) => 722,
+    
+    chr(198) => 1000,
+    chr(199) => 722,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 389,
+    chr(205) => 389,
+    chr(206) => 389,
+    chr(207) => 389,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 778,
+    chr(211) => 778,
+    chr(212) => 778,
+    chr(213) => 778,
+    chr(214) => 778,
+    chr(215) => 570,
+    chr(216) => 778,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 722,
+    chr(222) => 611,
+    chr(223) => 556,
+    chr(224) => 500,
+    chr(225) => 500,
+    chr(226) => 500,
+    chr(227) => 500,
+    chr(228) => 500,
+    chr(229) => 500,
+    chr(230) => 722,
+    chr(231) => 444,
+    chr(232) => 444,
+    chr(233) => 444,
+    chr(234) => 444,
+    chr(235) => 444,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 500,
+    chr(241) => 556,
+    
+    chr(242) => 500,
+    chr(243) => 500,
+    chr(244) => 500,
+    chr(245) => 500,
+    chr(246) => 500,
+    chr(247) => 570,
+    chr(248) => 500,
+    chr(249) => 556,
+    chr(250) => 556,
+    chr(251) => 556,
+    chr(252) => 556,
+    chr(253) => 500,
+    chr(254) => 556,
+    chr(255) => 500);
diff --git a/framework/File_PDF/PDF/fonts/timesbi.php b/framework/File_PDF/PDF/fonts/timesbi.php
new file mode 100644 (file)
index 0000000..4d8614f
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['timesBI'] = array(
+    chr(0) => 250,
+    chr(1) => 250,
+    chr(2) => 250,
+    chr(3) => 250,
+    chr(4) => 250,
+    chr(5) => 250,
+    chr(6) => 250,
+    chr(7) => 250,
+    chr(8) => 250,
+    chr(9) => 250,
+    chr(10) => 250,
+    chr(11) => 250,
+    chr(12) => 250,
+    chr(13) => 250,
+    chr(14) => 250,
+    chr(15) => 250,
+    chr(16) => 250,
+    chr(17) => 250,
+    chr(18) => 250,
+    chr(19) => 250,
+    chr(20) => 250,
+    chr(21) => 250,
+    
+    chr(22) => 250,
+    chr(23) => 250,
+    chr(24) => 250,
+    chr(25) => 250,
+    chr(26) => 250,
+    chr(27) => 250,
+    chr(28) => 250,
+    chr(29) => 250,
+    chr(30) => 250,
+    chr(31) => 250,
+    ' ' => 250,
+    '!' => 389,
+    '"' => 555,
+    '#' => 500,
+    '$' => 500,
+    '%' => 833,
+    '&' => 778,
+    '\'' => 278,
+    '(' => 333,
+    ')' => 333,
+    '*' => 500,
+    '+' => 570,
+    
+    ',' => 250,
+    '-' => 333,
+    '.' => 250,
+    '/' => 278,
+    '0' => 500,
+    '1' => 500,
+    '2' => 500,
+    '3' => 500,
+    '4' => 500,
+    '5' => 500,
+    '6' => 500,
+    '7' => 500,
+    '8' => 500,
+    '9' => 500,
+    ':' => 333,
+    ';' => 333,
+    '<' => 570,
+    '=' => 570,
+    '>' => 570,
+    '?' => 500,
+    '@' => 832,
+    'A' => 667,
+    
+    'B' => 667,
+    'C' => 667,
+    'D' => 722,
+    'E' => 667,
+    'F' => 667,
+    'G' => 722,
+    'H' => 778,
+    'I' => 389,
+    'J' => 500,
+    'K' => 667,
+    'L' => 611,
+    'M' => 889,
+    'N' => 722,
+    'O' => 722,
+    'P' => 611,
+    'Q' => 722,
+    'R' => 667,
+    'S' => 556,
+    'T' => 611,
+    'U' => 722,
+    'V' => 667,
+    'W' => 889,
+    
+    'X' => 667,
+    'Y' => 611,
+    'Z' => 611,
+    '[' => 333,
+    '\\' => 278,
+    ']' => 333,
+    '^' => 570,
+    '_' => 500,
+    '`' => 333,
+    'a' => 500,
+    'b' => 500,
+    'c' => 444,
+    'd' => 500,
+    'e' => 444,
+    'f' => 333,
+    'g' => 500,
+    'h' => 556,
+    'i' => 278,
+    'j' => 278,
+    'k' => 500,
+    'l' => 278,
+    'm' => 778,
+    
+    'n' => 556,
+    'o' => 500,
+    'p' => 500,
+    'q' => 500,
+    'r' => 389,
+    's' => 389,
+    't' => 278,
+    'u' => 556,
+    'v' => 444,
+    'w' => 667,
+    'x' => 500,
+    'y' => 444,
+    'z' => 389,
+    '{' => 348,
+    '|' => 220,
+    '}' => 348,
+    '~' => 570,
+    chr(127) => 350,
+    chr(128) => 500,
+    chr(129) => 350,
+    chr(130) => 333,
+    chr(131) => 500,
+    
+    chr(132) => 500,
+    chr(133) => 1000,
+    chr(134) => 500,
+    chr(135) => 500,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 556,
+    chr(139) => 333,
+    chr(140) => 944,
+    chr(141) => 350,
+    chr(142) => 611,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 333,
+    chr(146) => 333,
+    chr(147) => 500,
+    chr(148) => 500,
+    chr(149) => 350,
+    chr(150) => 500,
+    chr(151) => 1000,
+    chr(152) => 333,
+    chr(153) => 1000,
+    
+    chr(154) => 389,
+    chr(155) => 333,
+    chr(156) => 722,
+    chr(157) => 350,
+    chr(158) => 389,
+    chr(159) => 611,
+    chr(160) => 250,
+    chr(161) => 389,
+    chr(162) => 500,
+    chr(163) => 500,
+    chr(164) => 500,
+    chr(165) => 500,
+    chr(166) => 220,
+    chr(167) => 500,
+    chr(168) => 333,
+    chr(169) => 747,
+    chr(170) => 266,
+    chr(171) => 500,
+    chr(172) => 606,
+    chr(173) => 333,
+    chr(174) => 747,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 570,
+    chr(178) => 300,
+    chr(179) => 300,
+    chr(180) => 333,
+    chr(181) => 576,
+    chr(182) => 500,
+    chr(183) => 250,
+    chr(184) => 333,
+    chr(185) => 300,
+    chr(186) => 300,
+    chr(187) => 500,
+    chr(188) => 750,
+    chr(189) => 750,
+    chr(190) => 750,
+    chr(191) => 500,
+    chr(192) => 667,
+    chr(193) => 667,
+    chr(194) => 667,
+    chr(195) => 667,
+    chr(196) => 667,
+    chr(197) => 667,
+    
+    chr(198) => 944,
+    chr(199) => 667,
+    chr(200) => 667,
+    chr(201) => 667,
+    chr(202) => 667,
+    chr(203) => 667,
+    chr(204) => 389,
+    chr(205) => 389,
+    chr(206) => 389,
+    chr(207) => 389,
+    chr(208) => 722,
+    chr(209) => 722,
+    chr(210) => 722,
+    chr(211) => 722,
+    chr(212) => 722,
+    chr(213) => 722,
+    chr(214) => 722,
+    chr(215) => 570,
+    chr(216) => 722,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 611,
+    chr(222) => 611,
+    chr(223) => 500,
+    chr(224) => 500,
+    chr(225) => 500,
+    chr(226) => 500,
+    chr(227) => 500,
+    chr(228) => 500,
+    chr(229) => 500,
+    chr(230) => 722,
+    chr(231) => 444,
+    chr(232) => 444,
+    chr(233) => 444,
+    chr(234) => 444,
+    chr(235) => 444,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 500,
+    chr(241) => 556,
+    
+    chr(242) => 500,
+    chr(243) => 500,
+    chr(244) => 500,
+    chr(245) => 500,
+    chr(246) => 500,
+    chr(247) => 570,
+    chr(248) => 500,
+    chr(249) => 556,
+    chr(250) => 556,
+    chr(251) => 556,
+    chr(252) => 556,
+    chr(253) => 444,
+    chr(254) => 500,
+    chr(255) => 444);
diff --git a/framework/File_PDF/PDF/fonts/timesi.php b/framework/File_PDF/PDF/fonts/timesi.php
new file mode 100644 (file)
index 0000000..403b7e1
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['timesI'] = array(
+    chr(0) => 250,
+    chr(1) => 250,
+    chr(2) => 250,
+    chr(3) => 250,
+    chr(4) => 250,
+    chr(5) => 250,
+    chr(6) => 250,
+    chr(7) => 250,
+    chr(8) => 250,
+    chr(9) => 250,
+    chr(10) => 250,
+    chr(11) => 250,
+    chr(12) => 250,
+    chr(13) => 250,
+    chr(14) => 250,
+    chr(15) => 250,
+    chr(16) => 250,
+    chr(17) => 250,
+    chr(18) => 250,
+    chr(19) => 250,
+    chr(20) => 250,
+    chr(21) => 250,
+    
+    chr(22) => 250,
+    chr(23) => 250,
+    chr(24) => 250,
+    chr(25) => 250,
+    chr(26) => 250,
+    chr(27) => 250,
+    chr(28) => 250,
+    chr(29) => 250,
+    chr(30) => 250,
+    chr(31) => 250,
+    ' ' => 250,
+    '!' => 333,
+    '"' => 420,
+    '#' => 500,
+    '$' => 500,
+    '%' => 833,
+    '&' => 778,
+    '\'' => 214,
+    '(' => 333,
+    ')' => 333,
+    '*' => 500,
+    '+' => 675,
+    
+    ',' => 250,
+    '-' => 333,
+    '.' => 250,
+    '/' => 278,
+    '0' => 500,
+    '1' => 500,
+    '2' => 500,
+    '3' => 500,
+    '4' => 500,
+    '5' => 500,
+    '6' => 500,
+    '7' => 500,
+    '8' => 500,
+    '9' => 500,
+    ':' => 333,
+    ';' => 333,
+    '<' => 675,
+    '=' => 675,
+    '>' => 675,
+    '?' => 500,
+    '@' => 920,
+    'A' => 611,
+    
+    'B' => 611,
+    'C' => 667,
+    'D' => 722,
+    'E' => 611,
+    'F' => 611,
+    'G' => 722,
+    'H' => 722,
+    'I' => 333,
+    'J' => 444,
+    'K' => 667,
+    'L' => 556,
+    'M' => 833,
+    'N' => 667,
+    'O' => 722,
+    'P' => 611,
+    'Q' => 722,
+    'R' => 611,
+    'S' => 500,
+    'T' => 556,
+    'U' => 722,
+    'V' => 611,
+    'W' => 833,
+    
+    'X' => 611,
+    'Y' => 556,
+    'Z' => 556,
+    '[' => 389,
+    '\\' => 278,
+    ']' => 389,
+    '^' => 422,
+    '_' => 500,
+    '`' => 333,
+    'a' => 500,
+    'b' => 500,
+    'c' => 444,
+    'd' => 500,
+    'e' => 444,
+    'f' => 278,
+    'g' => 500,
+    'h' => 500,
+    'i' => 278,
+    'j' => 278,
+    'k' => 444,
+    'l' => 278,
+    'm' => 722,
+    
+    'n' => 500,
+    'o' => 500,
+    'p' => 500,
+    'q' => 500,
+    'r' => 389,
+    's' => 389,
+    't' => 278,
+    'u' => 500,
+    'v' => 444,
+    'w' => 667,
+    'x' => 444,
+    'y' => 444,
+    'z' => 389,
+    '{' => 400,
+    '|' => 275,
+    '}' => 400,
+    '~' => 541,
+    chr(127) => 350,
+    chr(128) => 500,
+    chr(129) => 350,
+    chr(130) => 333,
+    chr(131) => 500,
+    
+    chr(132) => 556,
+    chr(133) => 889,
+    chr(134) => 500,
+    chr(135) => 500,
+    chr(136) => 333,
+    chr(137) => 1000,
+    chr(138) => 500,
+    chr(139) => 333,
+    chr(140) => 944,
+    chr(141) => 350,
+    chr(142) => 556,
+    chr(143) => 350,
+    chr(144) => 350,
+    chr(145) => 333,
+    chr(146) => 333,
+    chr(147) => 556,
+    chr(148) => 556,
+    chr(149) => 350,
+    chr(150) => 500,
+    chr(151) => 889,
+    chr(152) => 333,
+    chr(153) => 980,
+    
+    chr(154) => 389,
+    chr(155) => 333,
+    chr(156) => 667,
+    chr(157) => 350,
+    chr(158) => 389,
+    chr(159) => 556,
+    chr(160) => 250,
+    chr(161) => 389,
+    chr(162) => 500,
+    chr(163) => 500,
+    chr(164) => 500,
+    chr(165) => 500,
+    chr(166) => 275,
+    chr(167) => 500,
+    chr(168) => 333,
+    chr(169) => 760,
+    chr(170) => 276,
+    chr(171) => 500,
+    chr(172) => 675,
+    chr(173) => 333,
+    chr(174) => 760,
+    chr(175) => 333,
+    
+    chr(176) => 400,
+    chr(177) => 675,
+    chr(178) => 300,
+    chr(179) => 300,
+    chr(180) => 333,
+    chr(181) => 500,
+    chr(182) => 523,
+    chr(183) => 250,
+    chr(184) => 333,
+    chr(185) => 300,
+    chr(186) => 310,
+    chr(187) => 500,
+    chr(188) => 750,
+    chr(189) => 750,
+    chr(190) => 750,
+    chr(191) => 500,
+    chr(192) => 611,
+    chr(193) => 611,
+    chr(194) => 611,
+    chr(195) => 611,
+    chr(196) => 611,
+    chr(197) => 611,
+    
+    chr(198) => 889,
+    chr(199) => 667,
+    chr(200) => 611,
+    chr(201) => 611,
+    chr(202) => 611,
+    chr(203) => 611,
+    chr(204) => 333,
+    chr(205) => 333,
+    chr(206) => 333,
+    chr(207) => 333,
+    chr(208) => 722,
+    chr(209) => 667,
+    chr(210) => 722,
+    chr(211) => 722,
+    chr(212) => 722,
+    chr(213) => 722,
+    chr(214) => 722,
+    chr(215) => 675,
+    chr(216) => 722,
+    chr(217) => 722,
+    chr(218) => 722,
+    chr(219) => 722,
+    
+    chr(220) => 722,
+    chr(221) => 556,
+    chr(222) => 611,
+    chr(223) => 500,
+    chr(224) => 500,
+    chr(225) => 500,
+    chr(226) => 500,
+    chr(227) => 500,
+    chr(228) => 500,
+    chr(229) => 500,
+    chr(230) => 667,
+    chr(231) => 444,
+    chr(232) => 444,
+    chr(233) => 444,
+    chr(234) => 444,
+    chr(235) => 444,
+    chr(236) => 278,
+    chr(237) => 278,
+    chr(238) => 278,
+    chr(239) => 278,
+    chr(240) => 500,
+    chr(241) => 500,
+    
+    chr(242) => 500,
+    chr(243) => 500,
+    chr(244) => 500,
+    chr(245) => 500,
+    chr(246) => 500,
+    chr(247) => 675,
+    chr(248) => 500,
+    chr(249) => 500,
+    chr(250) => 500,
+    chr(251) => 500,
+    chr(252) => 500,
+    chr(253) => 444,
+    chr(254) => 500,
+    chr(255) => 444);
diff --git a/framework/File_PDF/PDF/fonts/zapfdingbats.php b/framework/File_PDF/PDF/fonts/zapfdingbats.php
new file mode 100644 (file)
index 0000000..daa1a65
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/**
+ * @package File_PDF
+ */
+$font_widths['zapfdingbats'] = array(
+    chr(0) => 0,
+    chr(1) => 0,
+    chr(2) => 0,
+    chr(3) => 0,
+    chr(4) => 0,
+    chr(5) => 0,
+    chr(6) => 0,
+    chr(7) => 0,
+    chr(8) => 0,
+    chr(9) => 0,
+    chr(10) => 0,
+    chr(11) => 0,
+    chr(12) => 0,
+    chr(13) => 0,
+    chr(14) => 0,
+    chr(15) => 0,
+    chr(16) => 0,
+    chr(17) => 0,
+    chr(18) => 0,
+    chr(19) => 0,
+    chr(20) => 0,
+    chr(21) => 0,
+    
+    chr(22) => 0,
+    chr(23) => 0,
+    chr(24) => 0,
+    chr(25) => 0,
+    chr(26) => 0,
+    chr(27) => 0,
+    chr(28) => 0,
+    chr(29) => 0,
+    chr(30) => 0,
+    chr(31) => 0,
+    ' ' => 278,
+    '!' => 974,
+    '"' => 961,
+    '#' => 974,
+    '$' => 980,
+    '%' => 719,
+    '&' => 789,
+    '\'' => 790,
+    '(' => 791,
+    ')' => 690,
+    '*' => 960,
+    '+' => 939,
+    
+    ',' => 549,
+    '-' => 855,
+    '.' => 911,
+    '/' => 933,
+    '0' => 911,
+    '1' => 945,
+    '2' => 974,
+    '3' => 755,
+    '4' => 846,
+    '5' => 762,
+    '6' => 761,
+    '7' => 571,
+    '8' => 677,
+    '9' => 763,
+    ':' => 760,
+    ';' => 759,
+    '<' => 754,
+    '=' => 494,
+    '>' => 552,
+    '?' => 537,
+    '@' => 577,
+    'A' => 692,
+    
+    'B' => 786,
+    'C' => 788,
+    'D' => 788,
+    'E' => 790,
+    'F' => 793,
+    'G' => 794,
+    'H' => 816,
+    'I' => 823,
+    'J' => 789,
+    'K' => 841,
+    'L' => 823,
+    'M' => 833,
+    'N' => 816,
+    'O' => 831,
+    'P' => 923,
+    'Q' => 744,
+    'R' => 723,
+    'S' => 749,
+    'T' => 790,
+    'U' => 792,
+    'V' => 695,
+    'W' => 776,
+    
+    'X' => 768,
+    'Y' => 792,
+    'Z' => 759,
+    '[' => 707,
+    '\\' => 708,
+    ']' => 682,
+    '^' => 701,
+    '_' => 826,
+    '`' => 815,
+    'a' => 789,
+    'b' => 789,
+    'c' => 707,
+    'd' => 687,
+    'e' => 696,
+    'f' => 689,
+    'g' => 786,
+    'h' => 787,
+    'i' => 713,
+    'j' => 791,
+    'k' => 785,
+    'l' => 791,
+    'm' => 873,
+    
+    'n' => 761,
+    'o' => 762,
+    'p' => 762,
+    'q' => 759,
+    'r' => 759,
+    's' => 892,
+    't' => 892,
+    'u' => 788,
+    'v' => 784,
+    'w' => 438,
+    'x' => 138,
+    'y' => 277,
+    'z' => 415,
+    '{' => 392,
+    '|' => 392,
+    '}' => 668,
+    '~' => 668,
+    chr(127) => 0,
+    chr(128) => 390,
+    chr(129) => 390,
+    chr(130) => 317,
+    chr(131) => 317,
+    
+    chr(132) => 276,
+    chr(133) => 276,
+    chr(134) => 509,
+    chr(135) => 509,
+    chr(136) => 410,
+    chr(137) => 410,
+    chr(138) => 234,
+    chr(139) => 234,
+    chr(140) => 334,
+    chr(141) => 334,
+    chr(142) => 0,
+    chr(143) => 0,
+    chr(144) => 0,
+    chr(145) => 0,
+    chr(146) => 0,
+    chr(147) => 0,
+    chr(148) => 0,
+    chr(149) => 0,
+    chr(150) => 0,
+    chr(151) => 0,
+    chr(152) => 0,
+    chr(153) => 0,
+    
+    chr(154) => 0,
+    chr(155) => 0,
+    chr(156) => 0,
+    chr(157) => 0,
+    chr(158) => 0,
+    chr(159) => 0,
+    chr(160) => 0,
+    chr(161) => 732,
+    chr(162) => 544,
+    chr(163) => 544,
+    chr(164) => 910,
+    chr(165) => 667,
+    chr(166) => 760,
+    chr(167) => 760,
+    chr(168) => 776,
+    chr(169) => 595,
+    chr(170) => 694,
+    chr(171) => 626,
+    chr(172) => 788,
+    chr(173) => 788,
+    chr(174) => 788,
+    chr(175) => 788,
+    
+    chr(176) => 788,
+    chr(177) => 788,
+    chr(178) => 788,
+    chr(179) => 788,
+    chr(180) => 788,
+    chr(181) => 788,
+    chr(182) => 788,
+    chr(183) => 788,
+    chr(184) => 788,
+    chr(185) => 788,
+    chr(186) => 788,
+    chr(187) => 788,
+    chr(188) => 788,
+    chr(189) => 788,
+    chr(190) => 788,
+    chr(191) => 788,
+    chr(192) => 788,
+    chr(193) => 788,
+    chr(194) => 788,
+    chr(195) => 788,
+    chr(196) => 788,
+    chr(197) => 788,
+    
+    chr(198) => 788,
+    chr(199) => 788,
+    chr(200) => 788,
+    chr(201) => 788,
+    chr(202) => 788,
+    chr(203) => 788,
+    chr(204) => 788,
+    chr(205) => 788,
+    chr(206) => 788,
+    chr(207) => 788,
+    chr(208) => 788,
+    chr(209) => 788,
+    chr(210) => 788,
+    chr(211) => 788,
+    chr(212) => 894,
+    chr(213) => 838,
+    chr(214) => 1016,
+    chr(215) => 458,
+    chr(216) => 748,
+    chr(217) => 924,
+    chr(218) => 748,
+    chr(219) => 918,
+    
+    chr(220) => 927,
+    chr(221) => 928,
+    chr(222) => 928,
+    chr(223) => 834,
+    chr(224) => 873,
+    chr(225) => 828,
+    chr(226) => 924,
+    chr(227) => 924,
+    chr(228) => 917,
+    chr(229) => 930,
+    chr(230) => 931,
+    chr(231) => 463,
+    chr(232) => 883,
+    chr(233) => 836,
+    chr(234) => 836,
+    chr(235) => 867,
+    chr(236) => 867,
+    chr(237) => 696,
+    chr(238) => 696,
+    chr(239) => 874,
+    chr(240) => 0,
+    chr(241) => 874,
+    
+    chr(242) => 760,
+    chr(243) => 946,
+    chr(244) => 771,
+    chr(245) => 865,
+    chr(246) => 771,
+    chr(247) => 888,
+    chr(248) => 967,
+    chr(249) => 888,
+    chr(250) => 831,
+    chr(251) => 873,
+    chr(252) => 927,
+    chr(253) => 970,
+    chr(254) => 918,
+    chr(255) => 0);
diff --git a/framework/File_PDF/package.xml b/framework/File_PDF/package.xml
new file mode 100644 (file)
index 0000000..a7aa90a
--- /dev/null
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>File_PDF</name>
+ <channel>pear.php.net</channel>
+ <summary>PDF generation using only PHP.</summary>
+ <description>This package provides PDF generation using only PHP, without requiring any external libraries.</description>
+ <lead>
+  <name>Marko Djukic</name>
+  <user>mdjukic</user>
+  <email>mdjukic@horde.org</email>
+  <active>no</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>yunosh</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chagenbu</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-02-26</date>
+ <version>
+  <release>0.3.2</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Automatically restore font styles after adding headers and footers (PEAR Request #12310).
+* Pass $params to the class constructor to allow subclasses to receive and handle additional parameters (PEAR Request #12441).
+* Fix creating linked images (Horde Bug #5964).</notes>
+ <contents>
+  <dir name="/">
+   <dir name="PDF">
+    <dir name="fonts">
+     <file baseinstalldir="/File" name="courier.php" role="php" />
+     <file baseinstalldir="/File" name="helvetica.php" role="php" />
+     <file baseinstalldir="/File" name="helveticab.php" role="php" />
+     <file baseinstalldir="/File" name="helveticabi.php" role="php" />
+     <file baseinstalldir="/File" name="helveticai.php" role="php" />
+     <file baseinstalldir="/File" name="symbol.php" role="php" />
+     <file baseinstalldir="/File" name="times.php" role="php" />
+     <file baseinstalldir="/File" name="timesb.php" role="php" />
+     <file baseinstalldir="/File" name="timesbi.php" role="php" />
+     <file baseinstalldir="/File" name="timesi.php" role="php" />
+     <file baseinstalldir="/File" name="zapfdingbats.php" role="php" />
+    </dir> <!-- /PDF/fonts -->
+   </dir> <!-- /PDF -->
+   <dir name="tests">
+    <file baseinstalldir="/File" name="auto_break.phpt" role="test" />
+    <file baseinstalldir="/File" name="factory.phpt" role="test" />
+    <file baseinstalldir="/File" name="hello_world.phpt" role="test" />
+    <file baseinstalldir="/File" name="horde-power1.png" role="test" />
+    <file baseinstalldir="/File" name="links.phpt" role="test" />
+    <file baseinstalldir="/File" name="locale_floats.phpt" role="test" />
+    <file baseinstalldir="/File" name="pear12310.phpt" role="test" />
+    <file baseinstalldir="/File" name="text_color.phpt" role="test" />
+   </dir> <!-- /tests -->
+   <file baseinstalldir="/File" name="PDF.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+  <optional>
+   <package>
+    <name>HTTP_Download</name>
+    <channel>pear.php.net</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.3.1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-11-07</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Fixed escaping of parenthesis in PDF documents (PEAR Bug #12092).
+* Always reset position to the left margin before starting footers.
+* Fixed fill color overwriting text color (PEAR Bug #12310).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.3.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-09-14</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Add flush() method to allow processing of very large PDF files (Request #10077).
+* Fix underlined fonts (Bug #11447).
+* Workaround BC break in PHP 4.3.10 with some locales (Horde Bug #4094).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.2.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-01-22</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Catch errors from parsing images (Bug #8856).
+* Fix font width calculation of comma character (Andrew Teixeira, Bug #9595).
+* Add getPageWidth() and getPageHeight() methods (Request #9267).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-08-28</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Preserve font settings when adding new pages (Bug #2682).
+* Add setFontStyle() method (d.baechtold@unico.ch, Request #5230).
+* Allow all coordinates to be specified as negative values from the right or bottom edges (Request #5230).
+* Add setTextColor() to specify text colors different from fill colors (Request #1767).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-04-14</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Fixed loading of font metrics in setFont().
+* Fixed typo preventing setFillColor() and setDrawColor() from accepting any other colorspace than &apos;rgb&apos; (Horde Bug 1276).
+* Allow to use factory() method with custom class extended from File_PDF (Bug 1543).
+* Fixed typo in link() (Bug 1737).
+* Fixed save() method to actually save the whole document (Bug 1768).
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-06-04</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/File_PDF/tests/20k_c1.txt b/framework/File_PDF/tests/20k_c1.txt
new file mode 100644 (file)
index 0000000..6d5b295
--- /dev/null
@@ -0,0 +1,10 @@
+The year 1866 was marked by a bizarre development, an unexplained and downright inexplicable phenomenon that surely no one has forgotten. Without getting into those rumors that upset civilians in the seaports and deranged the public mind even far inland, it must be said that professional seamen were especially alarmed. Traders, shipowners, captains of vessels, skippers, and master mariners from Europe and America, naval officers from every country, and at their heels the various national governments on these two continents, were all extremely disturbed by the business.
+In essence, over a period of time several ships had encountered "an enormous thing" at sea, a long spindle-shaped object, sometimes giving off a phosphorescent glow, infinitely bigger and faster than any whale.
+The relevant data on this apparition, as recorded in various logbooks, agreed pretty closely as to the structure of the object or creature in question, its unprecedented speed of movement, its startling locomotive power, and the unique vitality with which it seemed to be gifted.  If it was a cetacean, it exceeded in bulk any whale previously classified by science.  No naturalist, neither Cuvier nor Lacépède, neither Professor Dumeril nor Professor de Quatrefages, would have accepted the existence of such a monster sight unseen -- specifically, unseen by their own scientific eyes.
+Striking an average of observations taken at different times -- rejecting those timid estimates that gave the object a length of 200 feet, and ignoring those exaggerated views that saw it as a mile wide and three long--you could still assert that this phenomenal creature greatly exceeded the dimensions of anything then known to ichthyologists, if it existed at all.
+Now then, it did exist, this was an undeniable fact; and since the human mind dotes on objects of wonder, you can understand the worldwide excitement caused by this unearthly apparition. As for relegating it to the realm of fiction, that charge had to be dropped.
+In essence, on July 20, 1866, the steamer Governor Higginson, from the Calcutta & Burnach Steam Navigation Co., encountered this moving mass five miles off the eastern shores of Australia. Captain Baker at first thought he was in the presence of an unknown reef; he was even about to fix its exact position when two waterspouts shot out of this inexplicable object and sprang hissing into the air some 150 feet.  So, unless this reef was subject to the intermittent eruptions of a geyser, the Governor Higginson had fair and honest dealings with some aquatic mammal, until then unknown, that could spurt from its blowholes waterspouts mixed with air and steam.
+Similar events were likewise observed in Pacific seas, on July 23 of the same year, by the Christopher Columbus from the West India & Pacific Steam Navigation Co.  Consequently, this extraordinary cetacean could transfer itself from one locality to another with startling swiftness, since within an interval of just three days, the Governor Higginson and the Christopher Columbus had observed it at two positions on the charts separated by a distance of more than 700 nautical leagues.
+Fifteen days later and 2,000 leagues farther, the Helvetia from the Compagnie Nationale and the Shannon from the Royal Mail line, running on opposite tacks in that part of the Atlantic lying between the United States and Europe, respectively signaled each other that the monster had been sighted in latitude 42 degrees 15' north and longitude 60 degrees 35' west of the meridian of Greenwich.  From their simultaneous observations, they were able to estimate the mammal's minimum length at more than 350 English feet; this was because both the Shannon and the Helvetia were of smaller dimensions, although each measured 100 meters stem to stern. Now then, the biggest whales, those rorqual whales that frequent the waterways of the Aleutian Islands, have never exceeded a length of 56 meters--if they reach even that.
+One after another, reports arrived that would profoundly affect public opinion:  new observations taken by the transatlantic liner Pereire, the Inman line's Etna running afoul of the monster, an official report drawn up by officers on the French frigate Normandy, dead-earnest reckonings obtained by the general staff of Commodore Fitz-James aboard the Lord Clyde. In lighthearted countries, people joked about this phenomenon, but such serious, practical countries as England, America, and Germany were deeply concerned.
+In every big city the monster was the latest rage; they sang about it in the coffee houses, they ridiculed it in the newspapers, they dramatized it in the theaters.  The tabloids found it a fine opportunity for hatching all sorts of hoaxes. In those newspapers short of copy, you saw the reappearance of every gigantic imaginary creature, from "Moby Dick," that dreadful white whale from the High Arctic regions, to the stupendous kraken whose tentacles could entwine a 500-ton craft and drag it into the ocean depths. They even reprinted reports from ancient times: the views of Aristotle and Pliny accepting the existence of such monsters, then the Norwegian stories of Bishop Pontoppidan, the narratives of Paul Egede, and finally the reports of Captain Harrington -- whose good faith is above suspicion--in which he claims he saw, while aboard the Castilian in 1857, one of those enormous serpents that, until then, had frequented only the seas of France's old extremist newspaper, The Constitutionalist.
diff --git a/framework/File_PDF/tests/20k_c2.txt b/framework/File_PDF/tests/20k_c2.txt
new file mode 100644 (file)
index 0000000..7b5c565
--- /dev/null
@@ -0,0 +1,23 @@
+During the period in which these developments were occurring, I had returned from a scientific undertaking organized to explore the Nebraska badlands in the United States. In my capacity as Assistant Professor at the Paris Museum of Natural History, I had been attached to this expedition by the French government. After spending six months in Nebraska, I arrived in New York laden with valuable collections near the end of March. My departure for France was set for early May. In the meantime, then, I was busy classifying my mineralogical, botanical, and zoological treasures when that incident took place with the Scotia.
+I was perfectly abreast of this question, which was the big news of the day, and how could I not have been? I had read and reread every American and European newspaper without being any farther along. This mystery puzzled me. Finding it impossible to form any views, I drifted from one extreme to the other. Something was out there, that much was certain, and any doubting Thomas was invited to place his finger on the Scotia's wound.
+When I arrived in New York, the question was at the boiling point. The hypothesis of a drifting islet or an elusive reef, put forward by people not quite in their right minds, was completely eliminated. And indeed, unless this reef had an engine in its belly, how could it move about with such prodigious speed?
+Also discredited was the idea of a floating hull or some other enormous wreckage, and again because of this speed of movement.
+So only two possible solutions to the question were left, creating two very distinct groups of supporters: on one side, those favoring a monster of colossal strength; on the other, those favoring an "underwater boat" of tremendous motor power.
+Now then, although the latter hypothesis was completely admissible, it couldn't stand up to inquiries conducted in both the New World and the Old. That a private individual had such a mechanism at his disposal was less than probable. Where and when had he built it, and how could he have built it in secret?
+Only some government could own such an engine of destruction, and in these disaster-filled times, when men tax their ingenuity to build increasingly powerful aggressive weapons, it was possible that, unknown to the rest of the world, some nation could have been testing such a fearsome machine. The Chassepot rifle led to the torpedo, and the torpedo has led to this underwater battering ram, which in turn will lead to the world putting its foot down. At least I hope it will.
+But this hypothesis of a war machine collapsed in the face of formal denials from the various governments. Since the public interest was at stake and transoceanic travel was suffering, the sincerity of these governments could not be doubted. Besides, how could the assembly of this underwater boat have escaped public notice? Keeping a secret under such circumstances would be difficult enough for an individual, and certainly impossible for a nation whose every move is under constant surveillance by rival powers.
+So, after inquiries conducted in England, France, Russia, Prussia, Spain, Italy, America, and even Turkey, the hypothesis of an underwater Monitor was ultimately rejected.
+After I arrived in New York, several people did me the honor of consulting me on the phenomenon in question. In France I had published a two-volume work, in quarto, entitled The Mysteries of the Great Ocean Depths. Well received in scholarly circles, this book had established me as a specialist in this pretty obscure field of natural history. My views were in demand. As long as I could deny the reality of the business, I confined myself to a flat "no comment." But soon, pinned to the wall, I had to explain myself straight out. And in this vein, "the honorable Pierre Aronnax, Professor at the Paris Museum," was summoned by The New York Herald to formulate his views no matter what.
+I complied. Since I could no longer hold my tongue, I let it wag. I discussed the question in its every aspect, both political and scientific, and this is an excerpt from the well-padded article I published in the issue of April 30.
+
+"Therefore," I wrote, "after examining these different hypotheses one by one, we are forced, every other supposition having been refuted, to accept the existence of an extremely powerful marine animal.
+"The deepest parts of the ocean are totally unknown to us. No soundings have been able to reach them. What goes on in those distant depths? What creatures inhabit, or could inhabit, those regions twelve or fifteen miles beneath the surface of the water? What is the constitution of these animals? It's almost beyond conjecture.
+"However, the solution to this problem submitted to me can take the form of a choice between two alternatives.
+"Either we know every variety of creature populating our planet, or we do not.
+"If we do not know every one of them, if nature still keeps ichthyological secrets from us, nothing is more admissible than to accept the existence of fish or cetaceans of new species or even new genera, animals with a basically 'cast-iron' constitution that inhabit strata beyond the reach of our soundings, and which some development or other, an urge or a whim if you prefer, can bring to the upper level of the ocean for long intervals.
+"If, on the other hand, we do know every living species, we must look for the animal in question among those marine creatures already cataloged, and in this event I would be inclined to accept the existence of a giant narwhale.
+"The common narwhale, or sea unicorn, often reaches a length of sixty feet. Increase its dimensions fivefold or even tenfold, then give this cetacean a strength in proportion to its size while enlarging its offensive weapons, and you have the animal we're looking for. It would have the proportions determined by the officers of the Shannon, the instrument needed to perforate the Scotia, and the power to pierce a steamer's hull.
+"In essence, the narwhale is armed with a sort of ivory sword, or lance, as certain naturalists have expressed it. It's a king-sized tooth as hard as steel. Some of these teeth have been found buried in the bodies of baleen whales, which the narwhale attacks with invariable success. Others have been wrenched, not without difficulty, from the undersides of vessels that narwhales have pierced clean through, as a gimlet pierces a wine barrel. The museum at the Faculty of Medicine in Paris owns one of these tusks with a length of 2.25 meters and a width at its base of forty-eight centimeters!
+"All right then! Imagine this weapon to be ten times stronger and the animal ten times more powerful, launch it at a speed of twenty miles per hour, multiply its mass times its velocity, and you get just the collision we need to cause the specified catastrophe.
+"So, until information becomes more abundant, I plump for a sea unicorn of colossal dimensions, no longer armed with a mere lance but with an actual spur, like ironclad frigates or those warships called 'rams,' whose mass and motor power it would possess simultaneously.
+"This inexplicable phenomenon is thus explained away--unless it's something else entirely, which, despite everything that has been sighted, studied, explored and experienced, is still possible!"
diff --git a/framework/File_PDF/tests/auto_break.phpt b/framework/File_PDF/tests/auto_break.phpt
new file mode 100644 (file)
index 0000000..6ac7431
--- /dev/null
@@ -0,0 +1,122 @@
+--TEST--
+File_PDF: Automatic page break test
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../PDF.php';
+
+// Set up the pdf object.
+$pdf = &File_PDF::factory(array('format' => array(50, 50), 'unit' => 'pt'));
+// Deactivate compression.
+$pdf->setCompression(false);
+// Set margins.
+$pdf->setMargins(0, 0);
+// Enable automatic page breaks.
+$pdf->setAutoPageBreak(true);
+// Start the document.
+$pdf->open();
+// Start a page.
+$pdf->addPage();
+// Set font to Courier 8 pt.
+$pdf->setFont('Courier', '', 10);
+// Write 7 lines
+$pdf->write(10, "Hello\nHello\nHello\nHello\nHello\nHello\nHello\n");
+// Print the generated file.
+echo $pdf->getOutput();
+
+?>
+--EXPECTF--
+%PDF-1.3
+3 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 4 0 R>>
+endobj
+4 0 obj
+<</Length 184>>
+stream
+2 J
+0.57 w
+BT /F1 10.00 Tf ET
+BT 2.83 42.00 Td (Hello) Tj ET
+BT 2.83 32.00 Td (Hello) Tj ET
+BT 2.83 22.00 Td (Hello) Tj ET
+BT 2.83 12.00 Td (Hello) Tj ET
+BT 2.83 2.00 Td (Hello) Tj ET
+
+endstream
+endobj
+5 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 6 0 R>>
+endobj
+6 0 obj
+<</Length 92>>
+stream
+2 J
+0.57 w
+BT /F1 10.00 Tf ET
+BT 2.83 42.00 Td (Hello) Tj ET
+BT 2.83 32.00 Td (Hello) Tj ET
+
+endstream
+endobj
+1 0 obj
+<</Type /Pages
+/Kids [3 0 R 5 0 R ]
+/Count 2
+/MediaBox [0 0 50.00 50.00]
+>>
+endobj
+7 0 obj
+<</Type /Font
+/BaseFont /Courier
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+2 0 obj
+<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+/Font <<
+/F1 7 0 R
+>>
+>>
+endobj
+8 0 obj
+<<
+/Producer (Horde PDF)
+/CreationDate (D:%d)
+>>
+endobj
+9 0 obj
+<<
+/Type /Catalog
+/Pages 1 0 R
+/OpenAction [3 0 R /FitH null]
+/PageLayout /OneColumn
+>>
+endobj
+xref
+0 10
+0000000000 65535 f 
+0000000538 00000 n 
+0000000723 00000 n 
+0000000009 00000 n 
+0000000087 00000 n 
+0000000320 00000 n 
+0000000398 00000 n 
+0000000629 00000 n 
+0000000811 00000 n 
+0000000887 00000 n 
+trailer
+<<
+/Size 10
+/Root 9 0 R
+/Info 8 0 R
+>>
+startxref
+990
+%%EOF
diff --git a/framework/File_PDF/tests/factory.phpt b/framework/File_PDF/tests/factory.phpt
new file mode 100644 (file)
index 0000000..987ae14
--- /dev/null
@@ -0,0 +1,63 @@
+--TEST--
+File_PDF: factory() test
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../PDF.php';
+
+/* Old signature. */
+$pdf = &File_PDF::factory('L', 'pt', 'A3');
+var_dump($pdf->_default_orientation);
+var_dump($pdf->_scale);
+var_dump($pdf->fwPt);
+var_dump($pdf->fhPt);
+$pdf = &File_PDF::factory('L', 'pt');
+var_dump($pdf->_default_orientation);
+var_dump($pdf->_scale);
+var_dump($pdf->fwPt);
+var_dump($pdf->fhPt);
+
+/* New signature. */
+$pdf = &File_PDF::factory(array('orientation' => 'L', 'unit' => 'pt', 'format' => 'A3'));
+var_dump($pdf->_default_orientation);
+var_dump($pdf->_scale);
+var_dump($pdf->fwPt);
+var_dump($pdf->fhPt);
+$pdf = &File_PDF::factory();
+var_dump($pdf->_default_orientation);
+var_dump(abs($pdf->_scale - 2.8346456692913) < 0.000001);
+var_dump($pdf->fwPt);
+var_dump($pdf->fhPt);
+
+/* Custom class. */
+class MyPDF extends File_PDF {}
+$pdf = &File_PDF::factory(array(), 'MyPDF');
+var_dump(strtolower(get_class($pdf)));
+var_dump($pdf->_default_orientation);
+var_dump(abs($pdf->_scale - 2.8346456692913) < 0.000001);
+var_dump($pdf->fwPt);
+var_dump($pdf->fhPt);
+
+?>
+--EXPECT--
+string(1) "L"
+int(1)
+float(841.89)
+float(1190.55)
+string(1) "L"
+int(1)
+float(595.28)
+float(841.89)
+string(1) "L"
+int(1)
+float(841.89)
+float(1190.55)
+string(1) "P"
+bool(true)
+float(595.28)
+float(841.89)
+string(5) "mypdf"
+string(1) "P"
+bool(true)
+float(595.28)
+float(841.89)
diff --git a/framework/File_PDF/tests/hello_world.phpt b/framework/File_PDF/tests/hello_world.phpt
new file mode 100644 (file)
index 0000000..6ee0ff9
Binary files /dev/null and b/framework/File_PDF/tests/hello_world.phpt differ
diff --git a/framework/File_PDF/tests/horde-power1.png b/framework/File_PDF/tests/horde-power1.png
new file mode 100644 (file)
index 0000000..eea229c
Binary files /dev/null and b/framework/File_PDF/tests/horde-power1.png differ
diff --git a/framework/File_PDF/tests/links.phpt b/framework/File_PDF/tests/links.phpt
new file mode 100644 (file)
index 0000000..aae9ec5
--- /dev/null
@@ -0,0 +1,153 @@
+--TEST--
+File_PDF: Link tests
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../PDF.php';
+
+// Set up the pdf object.
+$pdf = &File_PDF::factory(array('orientation' => 'P', 'format' => 'A4'));
+// Start the document.
+$pdf->open();
+// Deactivate compression.
+$pdf->setCompression(false);
+// Start a page.
+$pdf->addPage();
+// Set font to Helvetica 12 pt.
+$pdf->setFont('Helvetica', 'U', 12);
+// Write linked text.
+$pdf->write(15, 'Horde', 'http://www.horde.org');
+// Add line break.
+$pdf->write(15, "\n");
+// Write linked text.
+$link = $pdf->addLink();
+$pdf->write(15, 'here', $link);
+// Start next page.
+$pdf->addPage();
+// Add link anchor.
+$pdf->setLink($link);
+// Create linked image.
+$pdf->image(dirname(__FILE__) . '/horde-power1.png', 15, 15, 0, 0, '', 'http://pear.horde.org/');
+// Print the generated file.
+echo $pdf->getOutput();
+
+?>
+--EXPECTF--
+%PDF-1.3
+3 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Annots [<</Type /Annot /Subtype /Link /Rect [31.19 798.28 63.86 786.28] /Border [0 0 0] /A <</S /URI /URI (http://www.horde.org)>>>><</Type /Annot /Subtype /Link /Rect [31.19 755.76 55.20 743.76] /Border [0 0 0] /Dest [5 0 R /XYZ 0 841.89 null]>>]
+/Contents 4 0 R>>
+endobj
+4 0 obj
+<</Length 155>>
+stream
+2 J
+0.57 w
+BT /F1 12.00 Tf ET
+BT 31.19 788.68 Td (Horde) Tj ET 31.19 787.48 32.68 -0.60 re f
+BT 31.19 746.16 Td (here) Tj ET 31.19 744.96 24.01 -0.60 re f
+
+endstream
+endobj
+5 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Annots [<</Type /Annot /Subtype /Link /Rect [42.52 799.37 126.52 768.37] /Border [0 0 0] /A <</S /URI /URI (http://pear.horde.org/)>>>>]
+/Contents 6 0 R>>
+endobj
+6 0 obj
+<</Length 73>>
+stream
+2 J
+0.57 w
+BT /F1 12.00 Tf ET
+q 84.00 0 0 31.00 42.52 768.37 cm /I1 Do Q
+
+endstream
+endobj
+1 0 obj
+<</Type /Pages
+/Kids [3 0 R 5 0 R ]
+/Count 2
+/MediaBox [0 0 595.28 841.89]
+>>
+endobj
+7 0 obj
+<</Type /Font
+/BaseFont /Helvetica
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+8 0 obj
+<</Type /XObject
+/Subtype /Image
+/Width 84
+/Height 31
+/ColorSpace /DeviceRGB
+/BitsPerComponent 8
+/Filter /FlateDecode
+/DecodeParms <</Predictor 15 /Colors 3 /BitsPerComponent 8 /Columns 84>>
+/Length 2202>>
+stream
+%s
+%s
+%s
+%s
+%s
+%s
+%s
+%s
+%s
+endstream
+endobj
+2 0 obj
+<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+/Font <<
+/F1 7 0 R
+>>
+/XObject <<
+/I1 8 0 R
+>>
+>>
+endobj
+9 0 obj
+<<
+/Producer (Horde PDF)
+/CreationDate (D:%d)
+>>
+endobj
+10 0 obj
+<<
+/Type /Catalog
+/Pages 1 0 R
+/OpenAction [3 0 R /FitH null]
+/PageLayout /OneColumn
+>>
+endobj
+xref
+0 11
+0000000000 65535 f 
+0000000877 00000 n 
+0000003507 00000 n 
+0000000009 00000 n 
+0000000336 00000 n 
+0000000540 00000 n 
+0000000756 00000 n 
+0000000970 00000 n 
+0000001066 00000 n 
+0000003620 00000 n 
+0000003696 00000 n 
+trailer
+<<
+/Size 11
+/Root 10 0 R
+/Info 9 0 R
+>>
+startxref
+3800
+%%EOF
\ No newline at end of file
diff --git a/framework/File_PDF/tests/locale_floats.phpt b/framework/File_PDF/tests/locale_floats.phpt
new file mode 100644 (file)
index 0000000..ddfe95a
Binary files /dev/null and b/framework/File_PDF/tests/locale_floats.phpt differ
diff --git a/framework/File_PDF/tests/pear12310.phpt b/framework/File_PDF/tests/pear12310.phpt
new file mode 100644 (file)
index 0000000..61a67d4
--- /dev/null
@@ -0,0 +1,558 @@
+--TEST--
+PEAR Bug #12310
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/../PDF.php';
+
+class MyPDF extends File_PDF
+{
+    function header()
+    {
+        $this->setFont('Arial', 'B', 15);
+        $w = $this->getStringWidth($this->_info['title']) + 6;
+        $this->setX((210 - $w) / 2);
+        $this->setDrawColor('rgb', 0/255, 80/255, 180/255);
+        $this->setFillColor('rgb', 230/255, 230/255, 0/255);
+        $this->setTextColor('rgb', 220/255, 50/255, 50/255);
+        $this->setLineWidth(1);
+        $this->cell($w, 9, $this->_info['title'], 1, 1, 'C', 1);
+        $this->newLine(10);
+    }
+
+    function footer()
+    {
+        $this->setY(-15);
+        $this->setFont('Arial', 'I', 8);
+        $this->setTextColor('gray', 128/255);
+        $this->cell(0, 10, 'Page ' . $this->getPageNo(), 0, 0, 'C');
+    }
+
+    function chapterTitle($num, $label)
+    {
+        $this->setFont('Arial', '', 12);
+        $this->setFillColor('rgb', 200/255, 220/255, 255/255);
+        $this->cell(0, 6, "Chapter $num : $label", 0, 1, 'L', 1);
+        $this->newLine(4);
+    }
+
+    function chapterBody($file)
+    {
+        $text = file_get_contents(dirname(__FILE__) . '/' . $file);
+        $this->setFont('Times', '', 12);
+        $this->multiCell(0, 5, $text);
+        $this->newLine();
+        $this->setFont('', 'I');
+        $this->cell(0, 5, '(end of extract)');
+    }
+
+    function printChapter($num, $title, $file)
+    {
+        $this->addPage();
+        $this->chapterTitle($num, $title);
+        $this->chapterBody($file);
+    }
+
+}
+
+$pdf = MyPDF::factory(array('orientation' => 'P',
+                            'unit' => 'mm',
+                            'format' => 'A4'),
+                      'MyPDF');
+$pdf->setCompression(false);
+$pdf->setInfo('title', '20000 Leagues Under the Seas');
+$pdf->setInfo('author', 'Jules Verne');
+$pdf->printChapter(1, 'A RUNAWAY REEF', '20k_c1.txt');
+$pdf->printChapter(2, 'THE PROS AND CONS', '20k_c2.txt');
+echo $pdf->getOutput();
+
+?>
+--EXPECTF--
+%PDF-1.3
+3 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 4 0 R>>
+endobj
+4 0 obj
+<</Length 6962>>
+stream
+2 J
+0.57 w
+BT /F1 15.00 Tf ET
+0.000 0.314 0.706 RG
+0.902 0.902 0.000 rg
+2.83 w
+179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q
+0.57 w
+BT /F2 12.00 Tf ET
+0 g
+0 G
+0.784 0.863 1.000 rg
+28.35 759.68 538.58 -17.01 re f q 0 g BT 31.19 747.58 Td (Chapter 1 : A RUNAWAY REEF) Tj ET Q
+BT /F3 12.00 Tf ET
+0.002 Tw
+q 0 g BT 31.19 720.65 Td (The year 1866 was marked by a bizarre development, an unexplained and downright inexplicable phenomenon) Tj ET Q
+1.585 Tw
+q 0 g BT 31.19 706.48 Td (that surely no one has forgotten. Without getting into those rumors that upset civilians in the seaports and) Tj ET Q
+1.022 Tw
+q 0 g BT 31.19 692.30 Td (deranged the public mind even far inland, it must be said that professional seamen were especially alarmed.) Tj ET Q
+2.336 Tw
+q 0 g BT 31.19 678.13 Td (Traders, shipowners, captains of vessels, skippers, and master mariners from Europe and America, naval) Tj ET Q
+0.314 Tw
+q 0 g BT 31.19 663.96 Td (officers from every country, and at their heels the various national governments on these two continents, were) Tj ET Q
+0 Tw
+q 0 g BT 31.19 649.78 Td (all extremely disturbed by the business.) Tj ET Q
+3.660 Tw
+q 0 g BT 31.19 635.61 Td (In essence, over a period of time several ships had encountered "an enormous thing" at sea, a long) Tj ET Q
+2.306 Tw
+q 0 g BT 31.19 621.44 Td (spindle-shaped object, sometimes giving off a phosphorescent glow, infinitely bigger and faster than any) Tj ET Q
+0 Tw
+q 0 g BT 31.19 607.26 Td (whale.) Tj ET Q
+0.589 Tw
+q 0 g BT 31.19 593.09 Td (The relevant data on this apparition, as recorded in various logbooks, agreed pretty closely as to the structure) Tj ET Q
+0.125 Tw
+q 0 g BT 31.19 578.92 Td (of the object or creature in question, its unprecedented speed of movement, its startling locomotive power, and) Tj ET Q
+1.556 Tw
+q 0 g BT 31.19 564.74 Td (the unique vitality with which it seemed to be gifted.  If it was a cetacean, it exceeded in bulk any whale) Tj ET Q
+1.192 Tw
+q 0 g BT 31.19 550.57 Td (previously classified by science.  No naturalist, neither Cuvier nor Lacépède, neither Professor Dumeril nor) Tj ET Q
+1.159 Tw
+q 0 g BT 31.19 536.40 Td (Professor de Quatrefages, would have accepted the existence of such a monster sight unseen -- specifically,) Tj ET Q
+0 Tw
+q 0 g BT 31.19 522.22 Td (unseen by their own scientific eyes.) Tj ET Q
+1.520 Tw
+q 0 g BT 31.19 508.05 Td (Striking an average of observations taken at different times -- rejecting those timid estimates that gave the) Tj ET Q
+2.544 Tw
+q 0 g BT 31.19 493.88 Td (object a length of 200 feet, and ignoring those exaggerated views that saw it as a mile wide and three) Tj ET Q
+1.356 Tw
+q 0 g BT 31.19 479.70 Td (long--you could still assert that this phenomenal creature greatly exceeded the dimensions of anything then) Tj ET Q
+0 Tw
+q 0 g BT 31.19 465.53 Td (known to ichthyologists, if it existed at all.) Tj ET Q
+0.232 Tw
+q 0 g BT 31.19 451.36 Td (Now then, it did exist, this was an undeniable fact; and since the human mind dotes on objects of wonder, you) Tj ET Q
+0.292 Tw
+q 0 g BT 31.19 437.18 Td (can understand the worldwide excitement caused by this unearthly apparition. As for relegating it to the realm) Tj ET Q
+0 Tw
+q 0 g BT 31.19 423.01 Td (of fiction, that charge had to be dropped.) Tj ET Q
+3.687 Tw
+q 0 g BT 31.19 408.84 Td (In essence, on July 20, 1866, the steamer Governor Higginson, from the Calcutta & Burnach Steam) Tj ET Q
+0.332 Tw
+q 0 g BT 31.19 394.66 Td (Navigation Co., encountered this moving mass five miles off the eastern shores of Australia. Captain Baker at) Tj ET Q
+0.413 Tw
+q 0 g BT 31.19 380.49 Td (first thought he was in the presence of an unknown reef; he was even about to fix its exact position when two) Tj ET Q
+0.593 Tw
+q 0 g BT 31.19 366.32 Td (waterspouts shot out of this inexplicable object and sprang hissing into the air some 150 feet.  So, unless this) Tj ET Q
+0.177 Tw
+q 0 g BT 31.19 352.14 Td (reef was subject to the intermittent eruptions of a geyser, the Governor Higginson had fair and honest dealings) Tj ET Q
+0.662 Tw
+q 0 g BT 31.19 337.97 Td (with some aquatic mammal, until then unknown, that could spurt from its blowholes waterspouts mixed with) Tj ET Q
+0 Tw
+q 0 g BT 31.19 323.80 Td (air and steam.) Tj ET Q
+2.548 Tw
+q 0 g BT 31.19 309.63 Td (Similar events were likewise observed in Pacific seas, on July 23 of the same year, by the Christopher) Tj ET Q
+1.355 Tw
+q 0 g BT 31.19 295.45 Td (Columbus from the West India & Pacific Steam Navigation Co.  Consequently, this extraordinary cetacean) Tj ET Q
+0.567 Tw
+q 0 g BT 31.19 281.28 Td (could transfer itself from one locality to another with startling swiftness, since within an interval of just three) Tj ET Q
+1.163 Tw
+q 0 g BT 31.19 267.11 Td (days, the Governor Higginson and the Christopher Columbus had observed it at two positions on the charts) Tj ET Q
+0 Tw
+q 0 g BT 31.19 252.93 Td (separated by a distance of more than 700 nautical leagues.) Tj ET Q
+1.734 Tw
+q 0 g BT 31.19 238.76 Td (Fifteen days later and 2,000 leagues farther, the Helvetia from the Compagnie Nationale and the Shannon) Tj ET Q
+0.050 Tw
+q 0 g BT 31.19 224.59 Td (from the Royal Mail line, running on opposite tacks in that part of the Atlantic lying between the United States) Tj ET Q
+0.167 Tw
+q 0 g BT 31.19 210.41 Td (and Europe, respectively signaled each other that the monster had been sighted in latitude 42 degrees 15' north) Tj ET Q
+0.551 Tw
+q 0 g BT 31.19 196.24 Td (and longitude 60 degrees 35' west of the meridian of Greenwich.  From their simultaneous observations, they) Tj ET Q
+0.341 Tw
+q 0 g BT 31.19 182.07 Td (were able to estimate the mammal's minimum length at more than 350 English feet; this was because both the) Tj ET Q
+0.146 Tw
+q 0 g BT 31.19 167.89 Td (Shannon and the Helvetia were of smaller dimensions, although each measured 100 meters stem to stern. Now) Tj ET Q
+0.377 Tw
+q 0 g BT 31.19 153.72 Td (then, the biggest whales, those rorqual whales that frequent the waterways of the Aleutian Islands, have never) Tj ET Q
+0 Tw
+q 0 g BT 31.19 139.55 Td (exceeded a length of 56 meters--if they reach even that.) Tj ET Q
+0.293 Tw
+q 0 g BT 31.19 125.37 Td (One after another, reports arrived that would profoundly affect public opinion:  new observations taken by the) Tj ET Q
+0.893 Tw
+q 0 g BT 31.19 111.20 Td (transatlantic liner Pereire, the Inman line's Etna running afoul of the monster, an official report drawn up by) Tj ET Q
+0.051 Tw
+q 0 g BT 31.19 97.03 Td (officers on the French frigate Normandy, dead-earnest reckonings obtained by the general staff of Commodore) Tj ET Q
+1.236 Tw
+q 0 g BT 31.19 82.85 Td (Fitz-James aboard the Lord Clyde. In lighthearted countries, people joked about this phenomenon, but such) Tj ET Q
+0 Tw
+q 0 g BT 31.19 68.68 Td (serious, practical countries as England, America, and Germany were deeply concerned.) Tj ET Q
+0.060 Tw
+0 Tw
+BT /F4 8.00 Tf ET
+q 0.502 g BT 284.96 25.95 Td (Page 1) Tj ET Q
+
+endstream
+endobj
+5 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 6 0 R>>
+endobj
+6 0 obj
+<</Length 1847>>
+stream
+2 J
+0.57 w
+BT /F3 12.00 Tf ET
+BT /F1 15.00 Tf ET
+0.000 0.314 0.706 RG
+0.902 0.902 0.000 rg
+2.83 w
+179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q
+0.57 w
+BT /F3 12.00 Tf ET
+0.784 0.863 1.000 rg
+0 G
+0.060 Tw
+q 0 g BT 31.19 749.00 Td (In every big city the monster was the latest rage; they sang about it in the coffee houses, they ridiculed it in the) Tj ET Q
+0.034 Tw
+q 0 g BT 31.19 734.82 Td (newspapers, they dramatized it in the theaters.  The tabloids found it a fine opportunity for hatching all sorts of) Tj ET Q
+1.315 Tw
+q 0 g BT 31.19 720.65 Td (hoaxes. In those newspapers short of copy, you saw the reappearance of every gigantic imaginary creature,) Tj ET Q
+0.742 Tw
+q 0 g BT 31.19 706.48 Td (from "Moby Dick," that dreadful white whale from the High Arctic regions, to the stupendous kraken whose) Tj ET Q
+1.315 Tw
+q 0 g BT 31.19 692.30 Td (tentacles could entwine a 500-ton craft and drag it into the ocean depths. They even reprinted reports from) Tj ET Q
+0.707 Tw
+q 0 g BT 31.19 678.13 Td (ancient times: the views of Aristotle and Pliny accepting the existence of such monsters, then the Norwegian) Tj ET Q
+0.936 Tw
+q 0 g BT 31.19 663.96 Td (stories of Bishop Pontoppidan, the narratives of Paul Egede, and finally the reports of Captain Harrington --) Tj ET Q
+0.980 Tw
+q 0 g BT 31.19 649.78 Td (whose good faith is above suspicion--in which he claims he saw, while aboard the Castilian in 1857, one of) Tj ET Q
+1.389 Tw
+q 0 g BT 31.19 635.61 Td (those enormous serpents that, until then, had frequented only the seas of France's old extremist newspaper,) Tj ET Q
+0 Tw
+q 0 g BT 31.19 621.44 Td (The Constitutionalist.
+) Tj ET Q
+BT /F5 12.00 Tf ET
+q 0 g BT 31.19 593.09 Td (\(end of extract\)) Tj ET Q
+BT /F4 8.00 Tf ET
+q 0.502 g BT 284.96 25.95 Td (Page 2) Tj ET Q
+
+endstream
+endobj
+7 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 8 0 R>>
+endobj
+8 0 obj
+<</Length 6813>>
+stream
+2 J
+0.57 w
+BT /F5 12.00 Tf ET
+BT /F1 15.00 Tf ET
+0.000 0.314 0.706 RG
+0.902 0.902 0.000 rg
+2.83 w
+179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q
+0.57 w
+BT /F5 12.00 Tf ET
+0.784 0.863 1.000 rg
+0 G
+BT /F2 12.00 Tf ET
+0.784 0.863 1.000 rg
+28.35 759.68 538.58 -17.01 re f q 0 g BT 31.19 747.58 Td (Chapter 2 : THE PROS AND CONS) Tj ET Q
+BT /F3 12.00 Tf ET
+1.002 Tw
+q 0 g BT 31.19 720.65 Td (During the period in which these developments were occurring, I had returned from a scientific undertaking) Tj ET Q
+0.608 Tw
+q 0 g BT 31.19 706.48 Td (organized to explore the Nebraska badlands in the United States. In my capacity as Assistant Professor at the) Tj ET Q
+1.687 Tw
+q 0 g BT 31.19 692.30 Td (Paris Museum of Natural History, I had been attached to this expedition by the French government. After) Tj ET Q
+2.020 Tw
+q 0 g BT 31.19 678.13 Td (spending six months in Nebraska, I arrived in New York laden with valuable collections near the end of) Tj ET Q
+1.649 Tw
+q 0 g BT 31.19 663.96 Td (March. My departure for France was set for early May. In the meantime, then, I was busy classifying my) Tj ET Q
+0 Tw
+q 0 g BT 31.19 649.78 Td (mineralogical, botanical, and zoological treasures when that incident took place with the Scotia.) Tj ET Q
+0.471 Tw
+q 0 g BT 31.19 635.61 Td (I was perfectly abreast of this question, which was the big news of the day, and how could I not have been? I) Tj ET Q
+0.782 Tw
+q 0 g BT 31.19 621.44 Td (had read and reread every American and European newspaper without being any farther along. This mystery) Tj ET Q
+0.516 Tw
+q 0 g BT 31.19 607.26 Td (puzzled me. Finding it impossible to form any views, I drifted from one extreme to the other. Something was) Tj ET Q
+1.601 Tw
+q 0 g BT 31.19 593.09 Td (out there, that much was certain, and any doubting Thomas was invited to place his finger on the Scotia's) Tj ET Q
+0 Tw
+q 0 g BT 31.19 578.92 Td (wound.) Tj ET Q
+1.249 Tw
+q 0 g BT 31.19 564.74 Td (When I arrived in New York, the question was at the boiling point. The hypothesis of a drifting islet or an) Tj ET Q
+1.561 Tw
+q 0 g BT 31.19 550.57 Td (elusive reef, put forward by people not quite in their right minds, was completely eliminated. And indeed,) Tj ET Q
+0 Tw
+q 0 g BT 31.19 536.40 Td (unless this reef had an engine in its belly, how could it move about with such prodigious speed?) Tj ET Q
+0.779 Tw
+q 0 g BT 31.19 522.22 Td (Also discredited was the idea of a floating hull or some other enormous wreckage, and again because of this) Tj ET Q
+0 Tw
+q 0 g BT 31.19 508.05 Td (speed of movement.) Tj ET Q
+1.114 Tw
+q 0 g BT 31.19 493.88 Td (So only two possible solutions to the question were left, creating two very distinct groups of supporters: on) Tj ET Q
+0.914 Tw
+q 0 g BT 31.19 479.70 Td (one side, those favoring a monster of colossal strength; on the other, those favoring an "underwater boat" of) Tj ET Q
+0 Tw
+q 0 g BT 31.19 465.53 Td (tremendous motor power.) Tj ET Q
+3.674 Tw
+q 0 g BT 31.19 451.36 Td (Now then, although the latter hypothesis was completely admissible, it couldn't stand up to inquiries) Tj ET Q
+0.227 Tw
+q 0 g BT 31.19 437.18 Td (conducted in both the New World and the Old. That a private individual had such a mechanism at his disposal) Tj ET Q
+0 Tw
+q 0 g BT 31.19 423.01 Td (was less than probable. Where and when had he built it, and how could he have built it in secret?) Tj ET Q
+0.395 Tw
+q 0 g BT 31.19 408.84 Td (Only some government could own such an engine of destruction, and in these disaster-filled times, when men) Tj ET Q
+1.331 Tw
+q 0 g BT 31.19 394.66 Td (tax their ingenuity to build increasingly powerful aggressive weapons, it was possible that, unknown to the) Tj ET Q
+0.106 Tw
+q 0 g BT 31.19 380.49 Td (rest of the world, some nation could have been testing such a fearsome machine. The Chassepot rifle led to the) Tj ET Q
+0.490 Tw
+q 0 g BT 31.19 366.32 Td (torpedo, and the torpedo has led to this underwater battering ram, which in turn will lead to the world putting) Tj ET Q
+0 Tw
+q 0 g BT 31.19 352.14 Td (its foot down. At least I hope it will.) Tj ET Q
+1.078 Tw
+q 0 g BT 31.19 337.97 Td (But this hypothesis of a war machine collapsed in the face of formal denials from the various governments.) Tj ET Q
+0.251 Tw
+q 0 g BT 31.19 323.80 Td (Since the public interest was at stake and transoceanic travel was suffering, the sincerity of these governments) Tj ET Q
+0.979 Tw
+q 0 g BT 31.19 309.63 Td (could not be doubted. Besides, how could the assembly of this underwater boat have escaped public notice?) Tj ET Q
+3.430 Tw
+q 0 g BT 31.19 295.45 Td (Keeping a secret under such circumstances would be difficult enough for an individual, and certainly) Tj ET Q
+0 Tw
+q 0 g BT 31.19 281.28 Td (impossible for a nation whose every move is under constant surveillance by rival powers.) Tj ET Q
+0.422 Tw
+q 0 g BT 31.19 267.11 Td (So, after inquiries conducted in England, France, Russia, Prussia, Spain, Italy, America, and even Turkey, the) Tj ET Q
+0 Tw
+q 0 g BT 31.19 252.93 Td (hypothesis of an underwater Monitor was ultimately rejected.) Tj ET Q
+2.481 Tw
+q 0 g BT 31.19 238.76 Td (After I arrived in New York, several people did me the honor of consulting me on the phenomenon in) Tj ET Q
+0.569 Tw
+q 0 g BT 31.19 224.59 Td (question. In France I had published a two-volume work, in quarto, entitled The Mysteries of the Great Ocean) Tj ET Q
+0.862 Tw
+q 0 g BT 31.19 210.41 Td (Depths. Well received in scholarly circles, this book had established me as a specialist in this pretty obscure) Tj ET Q
+1.833 Tw
+q 0 g BT 31.19 196.24 Td (field of natural history. My views were in demand. As long as I could deny the reality of the business, I) Tj ET Q
+0.058 Tw
+q 0 g BT 31.19 182.07 Td (confined myself to a flat "no comment." But soon, pinned to the wall, I had to explain myself straight out. And) Tj ET Q
+1.637 Tw
+q 0 g BT 31.19 167.89 Td (in this vein, "the honorable Pierre Aronnax, Professor at the Paris Museum," was summoned by The New) Tj ET Q
+0 Tw
+q 0 g BT 31.19 153.72 Td (York Herald to formulate his views no matter what.) Tj ET Q
+0.697 Tw
+q 0 g BT 31.19 139.55 Td (I complied. Since I could no longer hold my tongue, I let it wag. I discussed the question in its every aspect,) Tj ET Q
+0.017 Tw
+q 0 g BT 31.19 125.37 Td (both political and scientific, and this is an excerpt from the well-padded article I published in the issue of April) Tj ET Q
+0 Tw
+q 0 g BT 31.19 111.20 Td (30.) Tj ET Q
+2.226 Tw
+q 0 g BT 31.19 82.85 Td ("Therefore," I wrote, "after examining these different hypotheses one by one, we are forced, every other) Tj ET Q
+0 Tw
+q 0 g BT 31.19 68.68 Td (supposition having been refuted, to accept the existence of an extremely powerful marine animal.) Tj ET Q
+0.550 Tw
+0 Tw
+BT /F4 8.00 Tf ET
+q 0.502 g BT 284.96 25.95 Td (Page 3) Tj ET Q
+
+endstream
+endobj
+9 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 10 0 R>>
+endobj
+10 0 obj
+<</Length 4707>>
+stream
+2 J
+0.57 w
+BT /F3 12.00 Tf ET
+BT /F1 15.00 Tf ET
+0.000 0.314 0.706 RG
+0.902 0.902 0.000 rg
+2.83 w
+179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q
+0.57 w
+BT /F3 12.00 Tf ET
+0.784 0.863 1.000 rg
+0 G
+0.550 Tw
+q 0 g BT 31.19 749.00 Td ("The deepest parts of the ocean are totally unknown to us. No soundings have been able to reach them. What) Tj ET Q
+0.352 Tw
+q 0 g BT 31.19 734.82 Td (goes on in those distant depths? What creatures inhabit, or could inhabit, those regions twelve or fifteen miles) Tj ET Q
+0 Tw
+q 0 g BT 31.19 720.65 Td (beneath the surface of the water? What is the constitution of these animals? It's almost beyond conjecture.) Tj ET Q
+3.495 Tw
+q 0 g BT 31.19 706.48 Td ("However, the solution to this problem submitted to me can take the form of a choice between two) Tj ET Q
+0 Tw
+q 0 g BT 31.19 692.30 Td (alternatives.) Tj ET Q
+q 0 g BT 31.19 678.13 Td ("Either we know every variety of creature populating our planet, or we do not.) Tj ET Q
+1.250 Tw
+q 0 g BT 31.19 663.96 Td ("If we do not know every one of them, if nature still keeps ichthyological secrets from us, nothing is more) Tj ET Q
+0.231 Tw
+q 0 g BT 31.19 649.78 Td (admissible than to accept the existence of fish or cetaceans of new species or even new genera, animals with a) Tj ET Q
+3.022 Tw
+q 0 g BT 31.19 635.61 Td (basically 'cast-iron' constitution that inhabit strata beyond the reach of our soundings, and which some) Tj ET Q
+1.589 Tw
+q 0 g BT 31.19 621.44 Td (development or other, an urge or a whim if you prefer, can bring to the upper level of the ocean for long) Tj ET Q
+0 Tw
+q 0 g BT 31.19 607.26 Td (intervals.) Tj ET Q
+0.321 Tw
+q 0 g BT 31.19 593.09 Td ("If, on the other hand, we do know every living species, we must look for the animal in question among those) Tj ET Q
+1.409 Tw
+q 0 g BT 31.19 578.92 Td (marine creatures already cataloged, and in this event I would be inclined to accept the existence of a giant) Tj ET Q
+0 Tw
+q 0 g BT 31.19 564.74 Td (narwhale.) Tj ET Q
+0.008 Tw
+q 0 g BT 31.19 550.57 Td ("The common narwhale, or sea unicorn, often reaches a length of sixty feet. Increase its dimensions fivefold or) Tj ET Q
+0.352 Tw
+q 0 g BT 31.19 536.40 Td (even tenfold, then give this cetacean a strength in proportion to its size while enlarging its offensive weapons,) Tj ET Q
+1.251 Tw
+q 0 g BT 31.19 522.22 Td (and you have the animal we're looking for. It would have the proportions determined by the officers of the) Tj ET Q
+0 Tw
+q 0 g BT 31.19 508.05 Td (Shannon, the instrument needed to perforate the Scotia, and the power to pierce a steamer's hull.) Tj ET Q
+0.130 Tw
+q 0 g BT 31.19 493.88 Td ("In essence, the narwhale is armed with a sort of ivory sword, or lance, as certain naturalists have expressed it.) Tj ET Q
+1.326 Tw
+q 0 g BT 31.19 479.70 Td (It's a king-sized tooth as hard as steel. Some of these teeth have been found buried in the bodies of baleen) Tj ET Q
+3.771 Tw
+q 0 g BT 31.19 465.53 Td (whales, which the narwhale attacks with invariable success. Others have been wrenched, not without) Tj ET Q
+0.119 Tw
+q 0 g BT 31.19 451.36 Td (difficulty, from the undersides of vessels that narwhales have pierced clean through, as a gimlet pierces a wine) Tj ET Q
+0.649 Tw
+q 0 g BT 31.19 437.18 Td (barrel. The museum at the Faculty of Medicine in Paris owns one of these tusks with a length of 2.25 meters) Tj ET Q
+0 Tw
+q 0 g BT 31.19 423.01 Td (and a width at its base of forty-eight centimeters!) Tj ET Q
+0.467 Tw
+q 0 g BT 31.19 408.84 Td ("All right then! Imagine this weapon to be ten times stronger and the animal ten times more powerful, launch) Tj ET Q
+0.980 Tw
+q 0 g BT 31.19 394.66 Td (it at a speed of twenty miles per hour, multiply its mass times its velocity, and you get just the collision we) Tj ET Q
+0 Tw
+q 0 g BT 31.19 380.49 Td (need to cause the specified catastrophe.) Tj ET Q
+1.067 Tw
+q 0 g BT 31.19 366.32 Td ("So, until information becomes more abundant, I plump for a sea unicorn of colossal dimensions, no longer) Tj ET Q
+0.631 Tw
+q 0 g BT 31.19 352.14 Td (armed with a mere lance but with an actual spur, like ironclad frigates or those warships called 'rams,' whose) Tj ET Q
+0 Tw
+q 0 g BT 31.19 337.97 Td (mass and motor power it would possess simultaneously.) Tj ET Q
+1.992 Tw
+q 0 g BT 31.19 323.80 Td ("This inexplicable phenomenon is thus explained away--unless it's something else entirely, which, despite) Tj ET Q
+0 Tw
+q 0 g BT 31.19 309.63 Td (everything that has been sighted, studied, explored and experienced, is still possible!"
+) Tj ET Q
+BT /F5 12.00 Tf ET
+q 0 g BT 31.19 281.28 Td (\(end of extract\)) Tj ET Q
+BT /F4 8.00 Tf ET
+q 0.502 g BT 284.96 25.95 Td (Page 4) Tj ET Q
+
+endstream
+endobj
+1 0 obj
+<</Type /Pages
+/Kids [3 0 R 5 0 R 7 0 R 9 0 R ]
+/Count 4
+/MediaBox [0 0 595.28 841.89]
+>>
+endobj
+11 0 obj
+<</Type /Font
+/BaseFont /Helvetica-Bold
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+12 0 obj
+<</Type /Font
+/BaseFont /Helvetica
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+13 0 obj
+<</Type /Font
+/BaseFont /Times-Roman
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+14 0 obj
+<</Type /Font
+/BaseFont /Helvetica-Oblique
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+15 0 obj
+<</Type /Font
+/BaseFont /Times-Italic
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+2 0 obj
+<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+/Font <<
+/F1 11 0 R
+/F2 12 0 R
+/F3 13 0 R
+/F4 14 0 R
+/F5 15 0 R
+>>
+>>
+endobj
+16 0 obj
+<<
+/Producer (Horde PDF)
+/Title (20000 Leagues Under the Seas)
+/Author (Jules Verne)
+/CreationDate (D:%d)
+>>
+endobj
+17 0 obj
+<<
+/Type /Catalog
+/Pages 1 0 R
+/OpenAction [3 0 R /FitH null]
+/PageLayout /OneColumn
+>>
+endobj
+xref
+0 18
+0000000000 65535 f 
+0000020852 00000 n 
+0000021460 00000 n 
+0000000009 00000 n 
+0000000087 00000 n 
+0000007099 00000 n 
+0000007177 00000 n 
+0000009074 00000 n 
+0000009152 00000 n 
+0000016015 00000 n 
+0000016094 00000 n 
+0000020957 00000 n 
+0000021059 00000 n 
+0000021156 00000 n 
+0000021255 00000 n 
+0000021360 00000 n 
+0000021593 00000 n 
+0000021730 00000 n 
+trailer
+<<
+/Size 18
+/Root 17 0 R
+/Info 16 0 R
+>>
+startxref
+21834
+%%EOF
diff --git a/framework/File_PDF/tests/text_color.phpt b/framework/File_PDF/tests/text_color.phpt
new file mode 100644 (file)
index 0000000..ba3da1c
--- /dev/null
@@ -0,0 +1,101 @@
+--TEST--
+File_PDF: Text colors.
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../PDF.php';
+
+// Set up the pdf object.
+$pdf = &File_PDF::factory();
+// Deactivate compression.
+$pdf->setCompression(false);
+// Start the document.
+$pdf->open();
+// Start a page.
+$pdf->addPage();
+// Set font to Helvetica bold 24 pt.
+$pdf->setFont('Helvetica', 'B', 48);
+// Set colors.
+$pdf->setDrawColor('rgb', 50, 0, 0);
+$pdf->setTextColor('rgb', 0, 50, 0);
+$pdf->setFillColor('rgb', 0, 0, 50);
+// Write text.
+$pdf->cell(0, 50, 'Hello Colors', 1, 0, 'C', 1);
+// Print the generated file.
+echo $pdf->getOutput();
+
+?>
+--EXPECTF--
+%PDF-1.3
+3 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Contents 4 0 R>>
+endobj
+4 0 obj
+<</Length 174>>
+stream
+2 J
+0.57 w
+BT /F1 48.00 Tf ET
+50.000 0.000 0.000 RG
+0.000 0.000 50.000 rg
+28.35 813.54 538.58 -141.73 re B q 0.000 50.000 0.000 rg BT 156.28 728.27 Td (Hello Colors) Tj ET Q
+
+endstream
+endobj
+1 0 obj
+<</Type /Pages
+/Kids [3 0 R ]
+/Count 1
+/MediaBox [0 0 595.28 841.89]
+>>
+endobj
+5 0 obj
+<</Type /Font
+/BaseFont /Helvetica-Bold
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+2 0 obj
+<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+/Font <<
+/F1 5 0 R
+>>
+>>
+endobj
+6 0 obj
+<<
+/Producer (Horde PDF)
+/CreationDate (D:%d)
+>>
+endobj
+7 0 obj
+<<
+/Type /Catalog
+/Pages 1 0 R
+/OpenAction [3 0 R /FitH null]
+/PageLayout /OneColumn
+>>
+endobj
+xref
+0 8
+0000000000 65535 f 
+0000000310 00000 n 
+0000000498 00000 n 
+0000000009 00000 n 
+0000000087 00000 n 
+0000000397 00000 n 
+0000000586 00000 n 
+0000000662 00000 n 
+trailer
+<<
+/Size 8
+/Root 7 0 R
+/Info 6 0 R
+>>
+startxref
+765
+%%EOF
diff --git a/framework/File_PDF/tests/underline.phpt b/framework/File_PDF/tests/underline.phpt
new file mode 100644 (file)
index 0000000..e2fa2b1
--- /dev/null
@@ -0,0 +1,99 @@
+--TEST--
+File_PDF: Underline test
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../PDF.php';
+
+// Set up the pdf object.
+$pdf = &File_PDF::factory(array('orientation' => 'P', 'format' => 'A4'));
+// Start the document.
+$pdf->open();
+// Deactivate compression.
+$pdf->setCompression(false);
+// Start a page.
+$pdf->addPage();
+// Set font to Courier 8 pt.
+$pdf->setFont('Helvetica', 'U', 12);
+// Write underlined text.
+$pdf->write(15, "Underlined\n");
+// Write linked text.
+$pdf->write(15, 'Horde', 'http://www.horde.org');
+// Print the generated file.
+echo $pdf->getOutput();
+
+?>
+--EXPECTF--
+%PDF-1.3
+3 0 obj
+<</Type /Page
+/Parent 1 0 R
+/Resources 2 0 R
+/Annots [<</Type /Annot /Subtype /Link /Rect [31.19 755.76 63.86 743.76] /Border [0 0 0] /A <</S /URI /URI (http://www.horde.org)>>>>]
+/Contents 4 0 R>>
+endobj
+4 0 obj
+<</Length 161>>
+stream
+2 J
+0.57 w
+BT /F1 12.00 Tf ET
+BT 31.19 788.68 Td (Underlined) Tj ET 31.19 787.48 58.02 -0.60 re f
+BT 31.19 746.16 Td (Horde) Tj ET 31.19 744.96 32.68 -0.60 re f
+
+endstream
+endobj
+1 0 obj
+<</Type /Pages
+/Kids [3 0 R ]
+/Count 1
+/MediaBox [0 0 595.28 841.89]
+>>
+endobj
+5 0 obj
+<</Type /Font
+/BaseFont /Helvetica
+/Subtype /Type1
+/Encoding /WinAnsiEncoding
+>>
+endobj
+2 0 obj
+<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+/Font <<
+/F1 5 0 R
+>>
+>>
+endobj
+6 0 obj
+<<
+/Producer (Horde PDF)
+/CreationDate (D:%d)
+>>
+endobj
+7 0 obj
+<<
+/Type /Catalog
+/Pages 1 0 R
+/OpenAction [3 0 R /FitH null]
+/PageLayout /OneColumn
+>>
+endobj
+xref
+0 8
+0000000000 65535 f 
+0000000432 00000 n 
+0000000615 00000 n 
+0000000009 00000 n 
+0000000222 00000 n 
+0000000519 00000 n 
+0000000703 00000 n 
+0000000779 00000 n 
+trailer
+<<
+/Size 8
+/Root 7 0 R
+/Info 6 0 R
+>>
+startxref
+882
+%%EOF
diff --git a/framework/Form/Form.php b/framework/Form/Form.php
new file mode 100644 (file)
index 0000000..659eecb
--- /dev/null
@@ -0,0 +1,830 @@
+<?php
+
+require_once 'Horde/Form/Type.php';
+require_once 'Horde/Form/Variable.php';
+
+/**
+ * Horde_Form Master Class.
+ *
+ * The Horde_Form:: package provides form rendering, validation, and
+ * other functionality for the Horde Application Framework.
+ *
+ * $Horde: framework/Form/Form.php,v 1.458 2009/10/14 00:05:36 chuck Exp $
+ *
+ * Copyright 2001-2007 Robert E. Coyle <robertecoyle@hotmail.com>
+ * Copyright 2001-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Robert E. Coyle <robertecoyle@hotmail.com>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Form
+ */
+class Horde_Form {
+
+    protected $_name = '';
+    protected $_title = '';
+    protected $_extra = '';
+    protected $_vars;
+    protected $_submit = array();
+    protected $_reset = false;
+    protected $_errors = array();
+    protected $_submitted = null;
+    public $_sections = array();
+    protected $_open_section = null;
+    protected $_currentSection = array();
+    protected $_variables = array();
+    protected $_hiddenVariables = array();
+    protected $_useFormToken = true;
+    protected $_autofilled = false;
+    protected $_enctype = null;
+    public $_help = false;
+
+    function Horde_Form(&$vars, $title = '', $name = null)
+    {
+        if (empty($name)) {
+            $name = Horde_String::lower(get_class($this));
+        }
+
+        $this->_vars = &$vars;
+        $this->_title = $title;
+        $this->_name = $name;
+    }
+
+    function __construct($vars, $title = '', $name = null)
+    {
+        $this->Horde_Form($vars, $title, $name);
+    }
+
+    function &singleton($form, &$vars, $title = '', $name = null)
+    {
+        static $instances = array();
+
+        $signature = serialize(array($form, $vars, $title, $name));
+        if (!isset($instances[$signature])) {
+            if (class_exists($form)) {
+                $instances[$signature] = new $form($vars, $title, $name);
+            } else {
+                $instances[$signature] = new Horde_Form($vars, $title, $name);
+            }
+        }
+
+        return $instances[$signature];
+    }
+
+    function setVars(&$vars)
+    {
+        $this->_vars = &$vars;
+    }
+
+    function getVars()
+    {
+        return $this->_vars;
+    }
+
+    function getTitle()
+    {
+        return $this->_title;
+    }
+
+    function setTitle($title)
+    {
+        $this->_title = $title;
+    }
+
+    function getExtra()
+    {
+        return $this->_extra;
+    }
+
+    function setExtra($extra)
+    {
+        $this->_extra = $extra;
+    }
+
+    function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Sets or gets whether the form should be verified by tokens.
+     * Tokens are used to verify that a form is only submitted once.
+     *
+     * @param boolean $token  If specified, sets whether to use form tokens.
+     *
+     * @return boolean  Whether form tokens are being used.
+     */
+    function useToken($token = null)
+    {
+        if (!is_null($token)) {
+            $this->_useFormToken = $token;
+        }
+        return $this->_useFormToken;
+    }
+
+    /**
+     * Get the renderer for this form, either a custom renderer or the
+     * standard one.
+     *
+     * To use a custom form renderer, your form class needs to
+     * override this function:
+     * <code>
+     * function &getRenderer()
+     * {
+     *     $r = new CustomFormRenderer();
+     *     return $r;
+     * }
+     * </code>
+     *
+     * ... where CustomFormRenderer is the classname of the custom
+     * renderer class, which should extend Horde_Form_Renderer.
+     *
+     * @param array $params  A hash of renderer-specific parameters.
+     *
+     * @return object Horde_Form_Renderer  The form renderer.
+     */
+    function getRenderer($params = array())
+    {
+        require_once 'Horde/Form/Renderer.php';
+        $renderer = new Horde_Form_Renderer($params);
+        return $renderer;
+    }
+
+    /**
+     * @throws Horde_Exception
+     */
+    function getType($type, $params = array())
+    {
+        $type_class = 'Horde_Form_Type_' . $type;
+        if (!class_exists($type_class)) {
+            throw new Horde_Exception(sprintf('Nonexistant class "%s" for field type "%s"', $type_class, $type));
+        }
+        $type_ob = new $type_class();
+        call_user_func_array(array($type_ob, 'init'), $params);
+        return $type_ob;
+    }
+
+    function setSection($section = '', $desc = '', $image = '', $expanded = true)
+    {
+        $this->_currentSection = $section;
+        if (!count($this->_sections) && !$this->getOpenSection()) {
+            $this->setOpenSection($section);
+        }
+        $this->_sections[$section]['desc'] = $desc;
+        $this->_sections[$section]['expanded'] = $expanded;
+        $this->_sections[$section]['image'] = $image;
+    }
+
+    function getSectionDesc($section)
+    {
+        return $this->_sections[$section]['desc'];
+    }
+
+    function getSectionImage($section)
+    {
+        return $this->_sections[$section]['image'];
+    }
+
+    function setOpenSection($section)
+    {
+        $this->_vars->set('__formOpenSection', $section);
+    }
+
+    function getOpenSection()
+    {
+        return $this->_vars->get('__formOpenSection');
+    }
+
+    function getSectionExpandedState($section, $boolean = false)
+    {
+        if ($boolean) {
+            /* Only the boolean value is required. */
+            return $this->_sections[$section]['expanded'];
+        }
+
+        /* Need to return the values for use in styles. */
+        if ($this->_sections[$section]['expanded']) {
+            return 'block';
+        } else {
+            return 'none';
+        }
+    }
+
+    /**
+     * TODO
+     */
+    function &addVariable($humanName, $varName, $type, $required,
+                          $readonly = false, $description = null,
+                          $params = array())
+    {
+        return $this->insertVariableBefore(null, $humanName, $varName, $type,
+                                           $required, $readonly, $description,
+                                           $params);
+    }
+
+    /**
+     * TODO
+     */
+    function &insertVariableBefore($before, $humanName, $varName, $type,
+                                   $required, $readonly = false,
+                                   $description = null, $params = array())
+    {
+        $type = &$this->getType($type, $params);
+        $var = new Horde_Form_Variable($humanName, $varName, $type,
+                                       $required, $readonly, $description);
+
+        /* Set the form object reference in the var. */
+        $var->setFormOb($this);
+
+        if ($var->getTypeName() == 'enum' &&
+            !strlen($type->getPrompt()) &&
+            count($var->getValues()) == 1) {
+            $vals = array_keys($var->getValues());
+            $this->_vars->add($var->varName, $vals[0]);
+            $var->_autofilled = true;
+        } elseif ($var->getTypeName() == 'file' ||
+                  $var->getTypeName() == 'image') {
+            $this->_enctype = 'multipart/form-data';
+        }
+        if (empty($this->_currentSection)) {
+            $this->_currentSection = '__base';
+        }
+
+        if (is_null($before)) {
+            $this->_variables[$this->_currentSection][] = &$var;
+        } else {
+            $num = 0;
+            while (isset($this->_variables[$this->_currentSection][$num]) &&
+                   $this->_variables[$this->_currentSection][$num]->getVarName() != $before) {
+                $num++;
+            }
+            if (!isset($this->_variables[$this->_currentSection][$num])) {
+                $this->_variables[$this->_currentSection][] = &$var;
+            } else {
+                $this->_variables[$this->_currentSection] = array_merge(
+                    array_slice($this->_variables[$this->_currentSection], 0, $num),
+                    array(&$var),
+                    array_slice($this->_variables[$this->_currentSection], $num));
+            }
+        }
+
+        return $var;
+    }
+
+    /**
+     * Removes a variable from the form.
+     *
+     * As only variables can be passed by reference, you need to call this
+     * method this way if want to pass a variable name:
+     * <code>
+     * $form->removeVariable($var = 'varname');
+     * </code>
+     *
+     * @param Horde_Form_Variable|string $var  Either the variable's name or
+     *                                         the variable to remove from the
+     *                                         form.
+     *
+     * @return boolean  True if the variable was found (and deleted).
+     */
+    function removeVariable(&$var)
+    {
+        foreach (array_keys($this->_variables) as $section) {
+            foreach (array_keys($this->_variables[$section]) as $i) {
+                if ((is_a($var, 'Horde_Form_Variable') && $this->_variables[$section][$i] === $var) ||
+                    ($this->_variables[$section][$i]->getVarName() == $var)) {
+                    // Slice out the variable to be removed.
+                    $this->_variables[$this->_currentSection] = array_merge(
+                        array_slice($this->_variables[$this->_currentSection], 0, $i),
+                        array_slice($this->_variables[$this->_currentSection], $i + 1));
+
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * TODO
+     */
+    function &addHidden($humanName, $varName, $type, $required,
+                        $readonly = false, $description = null,
+                        $params = array())
+    {
+        $type = &$this->getType($type, $params);
+        $var = new Horde_Form_Variable($humanName, $varName, $type,
+                                       $required, $readonly, $description);
+        $var->hide();
+        $this->_hiddenVariables[] = &$var;
+        return $var;
+    }
+
+    function &getVariables($flat = true, $withHidden = false)
+    {
+        if ($flat) {
+            $vars = array();
+            foreach ($this->_variables as $section) {
+                foreach ($section as $var) {
+                    $vars[] = $var;
+                }
+            }
+            if ($withHidden) {
+                foreach ($this->_hiddenVariables as $var) {
+                    $vars[] = $var;
+                }
+            }
+            return $vars;
+        } else {
+            return $this->_variables;
+        }
+    }
+
+    function setButtons($submit, $reset = false)
+    {
+        if ($submit === true || is_null($submit) || empty($submit)) {
+            /* Default to 'Submit'. */
+            $submit = array(_("Submit"));
+        } elseif (!is_array($submit)) {
+            /* Default to array if not passed. */
+            $submit = array($submit);
+        }
+        /* Only if $reset is strictly true insert default 'Reset'. */
+        if ($reset === true) {
+            $reset = _("Reset");
+        }
+
+        $this->_submit = $submit;
+        $this->_reset = $reset;
+    }
+
+    function appendButtons($submit)
+    {
+        if (!is_array($submit)) {
+            $submit = array($submit);
+        }
+
+        $this->_submit = array_merge($this->_submit, $submit);
+    }
+
+    function preserveVarByPost(&$vars, $varname, $alt_varname = '')
+    {
+        $value = $vars->getExists($varname, $wasset);
+
+        /* If an alternate name is given under which to preserve use that. */
+        if ($alt_varname) {
+            $varname = $alt_varname;
+        }
+
+        if ($wasset) {
+            $this->_preserveVarByPost($varname, $value);
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _preserveVarByPost($varname, $value)
+    {
+        if (is_array($value)) {
+            foreach ($value as $id => $val) {
+                $this->_preserveVarByPost($varname . '[' . $id . ']', $val);
+            }
+        } else {
+            $varname = htmlspecialchars($varname);
+            $value = htmlspecialchars($value);
+            printf('<input type="hidden" name="%s" value="%s" />' . "\n",
+                   $varname,
+                   $value);
+        }
+    }
+
+    function open(&$renderer, &$vars, $action, $method = 'get', $enctype = null)
+    {
+        if (is_null($enctype) && !is_null($this->_enctype)) {
+            $enctype = $this->_enctype;
+        }
+        $renderer->open($action, $method, $this->_name, $enctype);
+        $renderer->listFormVars($this);
+
+        if (!empty($this->_name)) {
+            $this->_preserveVarByPost('formname', $this->_name);
+        }
+
+        if ($this->_useFormToken) {
+            require_once 'Horde/Token.php';
+            $token = Horde_Token::generateId($this->_name);
+            $_SESSION['horde_form_secrets'][$token] = true;
+            $this->_preserveVarByPost($this->_name . '_formToken', $token);
+        }
+
+        /* Loop through vars and check for any special cases to preserve. */
+        $variables = $this->getVariables();
+        foreach ($variables as $var) {
+            /* Preserve value if change has to be tracked. */
+            if ($var->getOption('trackchange')) {
+                $varname = $var->getVarName();
+                $this->preserveVarByPost($vars, $varname, '__old_' . $varname);
+            }
+        }
+
+        foreach ($this->_hiddenVariables as $var) {
+            $this->preserveVarByPost($vars, $var->getVarName());
+        }
+    }
+
+    function close($renderer)
+    {
+        $renderer->close();
+    }
+
+    /**
+     * Renders the form for editing.
+     *
+     * @param Horde_Form_Renderer $renderer  A renderer instance, optional
+     *                                       since Horde 3.2.
+     * @param Variables $vars                A Variables instance, optional
+     *                                       since Horde 3.2.
+     * @param string $action                 The form action (url).
+     * @param string $method                 The form method, usually either
+     *                                       'get' or 'post'.
+     * @param string $enctype                The form encoding type. Determined
+     *                                       automatically if null.
+     * @param boolean $focus                 Focus the first form field?
+     */
+    function renderActive($renderer = null, $vars = null, $action = '',
+                          $method = 'get', $enctype = null, $focus = true)
+    {
+        if (is_null($renderer)) {
+            $renderer = $this->getRenderer();
+        }
+        if (is_null($vars)) {
+            $vars = $this->_vars;
+        }
+
+        if (is_null($enctype) && !is_null($this->_enctype)) {
+            $enctype = $this->_enctype;
+        }
+        $renderer->open($action, $method, $this->getName(), $enctype);
+        $renderer->listFormVars($this);
+
+        if (!empty($this->_name)) {
+            $this->_preserveVarByPost('formname', $this->_name);
+        }
+
+        if ($this->_useFormToken) {
+            require_once 'Horde/Token.php';
+            $token = Horde_Token::generateId($this->_name);
+            $_SESSION['horde_form_secrets'][$token] = true;
+            $this->_preserveVarByPost($this->_name . '_formToken', $token);
+        }
+
+        if (count($this->_sections)) {
+            $this->_preserveVarByPost('__formOpenSection', $this->getOpenSection());
+        }
+
+        /* Loop through vars and check for any special cases to
+         * preserve. */
+        $variables = $this->getVariables();
+        foreach ($variables as $var) {
+            /* Preserve value if change has to be tracked. */
+            if ($var->getOption('trackchange')) {
+                $varname = $var->getVarName();
+                $this->preserveVarByPost($vars, $varname, '__old_' . $varname);
+            }
+        }
+
+        foreach ($this->_hiddenVariables as $var) {
+            $this->preserveVarByPost($vars, $var->getVarName());
+        }
+
+        $renderer->beginActive($this->getTitle(), $this->getExtra());
+        $renderer->renderFormActive($this, $vars);
+        $renderer->submit($this->_submit, $this->_reset);
+        $renderer->end();
+        $renderer->close($focus);
+    }
+
+    /**
+     * Renders the form for displaying.
+     *
+     * @param Horde_Form_Renderer $renderer  A renderer instance, optional
+     *                                       since Horde 3.2.
+     * @param Variables $vars                A Variables instance, optional
+     *                                       since Horde 3.2.
+     */
+    function renderInactive($renderer = null, $vars = null)
+    {
+        if (is_null($renderer)) {
+            $renderer = $this->getRenderer();
+        }
+        if (is_null($vars)) {
+            $vars = $this->_vars;
+        }
+
+        $renderer->_name = $this->_name;
+        $renderer->beginInactive($this->getTitle(), $this->getExtra());
+        $renderer->renderFormInactive($this, $vars);
+        $renderer->end();
+    }
+
+    function preserve($vars)
+    {
+        if ($this->_useFormToken) {
+            require_once 'Horde/Token.php';
+            $token = Horde_Token::generateId($this->_name);
+            $_SESSION['horde_form_secrets'][$token] = true;
+            $this->_preserveVarByPost($this->_name . '_formToken', $token);
+        }
+
+        $variables = $this->getVariables();
+        foreach ($variables as $var) {
+            $varname = $var->getVarName();
+
+            /* Save value of individual components. */
+            switch ($var->getTypeName()) {
+            case 'passwordconfirm':
+            case 'emailconfirm':
+                $this->preserveVarByPost($vars, $varname . '[original]');
+                $this->preserveVarByPost($vars, $varname . '[confirm]');
+                break;
+
+            case 'monthyear':
+                $this->preserveVarByPost($vars, $varname . '[month]');
+                $this->preserveVarByPost($vars, $varname . '[year]');
+                break;
+
+            case 'monthdayyear':
+                $this->preserveVarByPost($vars, $varname . '[month]');
+                $this->preserveVarByPost($vars, $varname . '[day]');
+                $this->preserveVarByPost($vars, $varname . '[year]');
+                break;
+            }
+
+            $this->preserveVarByPost($vars, $varname);
+        }
+        foreach ($this->_hiddenVariables as $var) {
+            $this->preserveVarByPost($vars, $var->getVarName());
+        }
+    }
+
+    function unsetVars(&$vars)
+    {
+        foreach ($this->getVariables() as $var) {
+            $vars->remove($var->getVarName());
+        }
+    }
+
+    /**
+     * Validates the form, checking if it really has been submitted by calling
+     * isSubmitted() and if true does any onSubmit() calls for variable types
+     * in the form. The _submitted variable is then rechecked.
+     *
+     * @param Variables $vars       A Variables instance, optional since Horde
+     *                              3.2.
+     * @param boolean $canAutofill  Can the form be valid without being
+     *                              submitted?
+     *
+     * @return boolean  True if the form is valid.
+     */
+    function validate($vars = null, $canAutoFill = false)
+    {
+        if (is_null($vars)) {
+            $vars = $this->_vars;
+        }
+
+        /* Get submitted status. */
+        if ($this->isSubmitted() || $canAutoFill) {
+            /* Form was submitted or can autofill; check for any variable
+             * types' onSubmit(). */
+            $this->onSubmit($vars);
+
+            /* Recheck submitted status. */
+            if (!$this->isSubmitted() && !$canAutoFill) {
+                return false;
+            }
+        } else {
+            /* Form has not been submitted; return false. */
+            return false;
+        }
+
+        $message = '';
+        $this->_autofilled = true;
+
+        if ($this->_useFormToken) {
+            global $conf;
+            require_once 'Horde/Token.php';
+            if (isset($conf['token'])) {
+                /* If there is a configured token system, set it up. */
+                $tokenSource = &Horde_Token::singleton($conf['token']['driver'], Horde::getDriverConfig('token', $conf['token']['driver']));
+            } else {
+                /* Default to the file system if no config. */
+                $tokenSource = &Horde_Token::singleton('file');
+            }
+            $passedToken = $vars->get($this->_name . '_formToken');
+            if (!empty($passedToken) && !$tokenSource->verify($passedToken)) {
+                $this->_errors['_formToken'] = _("This form has already been processed.");
+            }
+            if (empty($_SESSION['horde_form_secrets'][$passedToken])) {
+                $this->_errors['_formSecret'] = _("Required secret is invalid - potentially malicious request.");
+            }
+        }
+
+        foreach ($this->getVariables() as $var) {
+            $this->_autofilled = $var->_autofilled && $this->_autofilled;
+            if (!$var->validate($vars, $message)) {
+                $this->_errors[$var->getVarName()] = $message;
+            }
+        }
+
+        if ($this->_autofilled) {
+            unset($this->_errors['_formToken']);
+        }
+
+        foreach ($this->_hiddenVariables as $var) {
+            if (!$var->validate($vars, $message)) {
+                $this->_errors[$var->getVarName()] = $message;
+            }
+        }
+
+        return $this->isValid();
+    }
+
+    function clearValidation()
+    {
+        $this->_errors = array();
+    }
+
+    function getError($var)
+    {
+        if (is_a($var, 'Horde_Form_Variable')) {
+            $name = $var->getVarName();
+        } else {
+            $name = $var;
+        }
+        return isset($this->_errors[$name]) ? $this->_errors[$name] : null;
+    }
+
+    function setError($var, $message)
+    {
+        if (is_a($var, 'Horde_Form_Variable')) {
+            $name = $var->getVarName();
+        } else {
+            $name = $var;
+        }
+        $this->_errors[$name] = $message;
+    }
+
+    function clearError($var)
+    {
+        if (is_a($var, 'Horde_Form_Variable')) {
+            $name = $var->getVarName();
+        } else {
+            $name = $var;
+        }
+        unset($this->_errors[$name]);
+    }
+
+    function isValid()
+    {
+        return ($this->_autofilled || count($this->_errors) == 0);
+    }
+
+    function execute()
+    {
+        Horde::logMessage('Warning: Horde_Form::execute() called, should be overridden', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+    }
+
+    /**
+     * Fetch the field values of the submitted form.
+     *
+     * @param Variables $vars  A Variables instance, optional since Horde 3.2.
+     * @param array $info      Array to be filled with the submitted field
+     *                         values.
+     */
+    function getInfo($vars, &$info)
+    {
+        if (is_null($vars)) {
+            $vars = $this->_vars;
+        }
+        $this->_getInfoFromVariables($this->getVariables(), $vars, $info);
+        $this->_getInfoFromVariables($this->_hiddenVariables, $vars, $info);
+    }
+
+    /**
+     * Fetch the field values from a given array of variables.
+     *
+     * @access private
+     *
+     * @param array  $variables  An array of Horde_Form_Variable objects to
+     *                           fetch from.
+     * @param object $vars       The Variables object.
+     * @param array  $info       The array to be filled with the submitted
+     *                           field values.
+     */
+    function _getInfoFromVariables($variables, &$vars, &$info)
+    {
+        foreach ($variables as $var) {
+            if ($var->isArrayVal()) {
+                $var->getInfo($vars, $values);
+                if (is_array($values)) {
+                    $varName = str_replace('[]', '', $var->getVarName());
+                    foreach ($values as $i => $val) {
+                        $info[$i][$varName] = $val;
+                    }
+                }
+            } else {
+                require_once 'Horde/Array.php';
+                if (Horde_Array::getArrayParts($var->getVarName(), $base, $keys)) {
+                    if (!isset($info[$base])) {
+                        $info[$base] = array();
+                    }
+                    $pointer = &$info[$base];
+                    while (count($keys)) {
+                        $key = array_shift($keys);
+                        if (!isset($pointer[$key])) {
+                            $pointer[$key] = array();
+                        }
+                        $pointer = &$pointer[$key];
+                    }
+                    $var->getInfo($vars, $pointer);
+                } else {
+                    $var->getInfo($vars, $info[$var->getVarName()]);
+                }
+            }
+        }
+    }
+
+    function hasHelp()
+    {
+        return $this->_help;
+    }
+
+    /**
+     * Determines if this form has been submitted or not. If the class
+     * var _submitted is null then it will check for the presence of
+     * the formname in the form variables.
+     *
+     * Other events can explicitly set the _submitted variable to
+     * false to indicate a form submit but not for actual posting of
+     * data (eg. onChange events to update the display of fields).
+     *
+     * @return boolean  True or false indicating if the form has been
+     *                  submitted.
+     */
+    function isSubmitted()
+    {
+        if (is_null($this->_submitted)) {
+            if ($this->_vars->get('formname') == $this->getName()) {
+                $this->_submitted = true;
+            } else {
+                $this->_submitted = false;
+            }
+        }
+
+        return $this->_submitted;
+    }
+
+    /**
+     * Checks if there is anything to do on the submission of the form by
+     * looping through each variable's onSubmit() function.
+     *
+     * @param Horde_Variables $vars
+     */
+    function onSubmit(&$vars)
+    {
+        /* Loop through all vars and check if there's anything to do on
+         * submit. */
+        $variables = $this->getVariables();
+        foreach ($variables as $var) {
+            $var->type->onSubmit($var, $vars);
+            /* If changes to var being tracked don't register the form as
+             * submitted if old value and new value differ. */
+            if ($var->getOption('trackchange')) {
+                $varname = $var->getVarName();
+                if (!is_null($vars->get('formname')) &&
+                    $vars->get($varname) != $vars->get('__old_' . $varname)) {
+                    $this->_submitted = false;
+                }
+            }
+        }
+    }
+
+    /**
+     * Explicitly sets the state of the form submit.
+     *
+     * An event can override the automatic determination of the submit state
+     * in the isSubmitted() function.
+     *
+     * @param boolean $state  Whether to set the state of the form as being
+     *                        submitted.
+     */
+    function setSubmitted($state = true)
+    {
+        $this->_submitted = $state;
+    }
+
+}
diff --git a/framework/Form/Form/Action.php b/framework/Form/Form/Action.php
new file mode 100644 (file)
index 0000000..ff5e211
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+/**
+ * The Horde_Form_Action class provides an API for adding actions to
+ * Horde_Form variables.
+ *
+ * $Horde: framework/Form/Form/Action.php,v 1.31 2009/01/06 17:49:17 jan Exp $
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action {
+
+    var $_id;
+    var $_params;
+    var $_trigger = null;
+
+    function Horde_Form_Action($params = null)
+    {
+        $this->_params = $params;
+        $this->_id = md5(mt_rand());
+    }
+
+    function getTrigger()
+    {
+        return $this->_trigger;
+    }
+
+    function id()
+    {
+        return $this->_id;
+    }
+
+    function getActionScript($form, $renderer, $varname)
+    {
+        return '';
+    }
+
+    function printJavaScript()
+    {
+    }
+
+    function _printJavaScriptStart()
+    {
+        echo '<script type="text/javascript"><!--';
+    }
+
+    function _printJavaScriptEnd()
+    {
+        echo '// --></script>';
+    }
+
+    function getTarget()
+    {
+        return isset($this->_params['target']) ? $this->_params['target'] : null;
+    }
+
+    function setValues(&$vars, $sourceVal, $index = null, $arrayVal = false)
+    {
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Form_Action instance
+     * based on $form.
+     *
+     * @param mixed $action  The type of concrete Horde_Form_Action subclass
+     *                       to return. If $action is an array, then we will look
+     *                       in $action[0]/lib/Form/Action/ for the subclass
+     *                       implementation named $action[1].php.
+     * @param array $params  A hash containing any additional configuration a
+     *                       form might need.
+     *
+     * @return Horde_Form_Action  The concrete Horde_Form_Action reference, or
+     *                            false on an error.
+     */
+    function &factory($action, $params = null)
+    {
+        if (is_array($action)) {
+            $app = $action[0];
+            $action = $action[1];
+        }
+
+        $action = basename($action);
+        $class = 'Horde_Form_Action_' . $action;
+        if (!class_exists($class)) {
+            if (!empty($app)) {
+                include_once $GLOBALS['registry']->get('fileroot', $app) . '/lib/Form/Action/' . $action . '.php';
+            } else {
+                include_once 'Horde/Form/Action/' . $action . '.php';
+            }
+        }
+
+        if (class_exists($class)) {
+            $instance = new $class($params);
+        } else {
+            $instance = PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+
+        return $instance;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete
+     * Horde_Form_Action instance based on $action. It will only
+     * create a new instance if no Horde_Form_Action instance with
+     * the same parameters currently exists.
+     *
+     * This should be used if multiple types of form renderers (and,
+     * thus, multiple Horde_Form_Action instances) are required.
+     *
+     * This method must be invoked as: $var =
+     * &Horde_Form_Action::singleton()
+     *
+     * @param mixed $action  The type of concrete Horde_Form_Action subclass to return.
+     *                       The code is dynamically included. If $action is an array,
+     *                       then we will look in $action[0]/lib/Form/Action/ for
+     *                       the subclass implementation named $action[1].php.
+     * @param array $params  A hash containing any additional configuration a
+     *                       form might need.
+     *
+     * @return Horde_Form_Action  The concrete Horde_Form_Action reference, or
+     *                            false on an error.
+     */
+    function &singleton($action, $params = null)
+    {
+        static $instances = array();
+
+        $signature = serialize(array($action, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &Horde_Form_Action::factory($action, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Form/Form/Action/ConditionalEnable.php b/framework/Form/Form/Action/ConditionalEnable.php
new file mode 100644 (file)
index 0000000..ccba9eb
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Horde_Form_Action_ConditionalEnable is a Horde_Form_Action that
+ * enables or disables an element based on the value of another element
+ *
+ * Format of the $params passed to the constructor:
+ * <pre>
+ *  $params = array(
+ *      'target'  => '[name of element this is conditional on]',
+ *      'enabled' => 'true' | 'false',
+ *      'values'  => array([target values to check])
+ *  );
+ * </pre>
+ *
+ * So $params = array('foo', 'true', array(1, 2)) will enable the field this
+ * action is attached to if the value of 'foo' is 1 or 2, and disable it
+ * otherwise.
+ *
+ * $Horde: framework/Form/Form/Action/ConditionalEnable.php,v 1.6 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Matt Kynaston <matt@kynx.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_ConditionalEnable extends Horde_Form_Action {
+
+    var $_trigger = array('onload');
+
+    function getActionScript(&$form, $renderer, $varname)
+    {
+        Horde::addScriptFile('form_helpers.js', 'horde');
+
+        $form_name = $form->getName();
+        $target = $this->_params['target'];
+        $enabled = $this->_params['enabled'];
+        if (!is_string($enabled)) {
+            $enabled = ($enabled) ? 'true' : 'false';
+        }
+        $vals = $this->_params['values'];
+        $vals = (is_array($vals)) ? $vals : array($vals);
+        $args = "'$varname', $enabled, '" . implode("','", $vals) . "'";
+
+        return "if (addEvent(document.getElementById('$form_name').$target, 'onchange', \"checkEnabled(this, $args);\")) { "
+            . "  checkEnabled(document.getElementById('$form_name').$varname, $args); };";
+    }
+
+}
diff --git a/framework/Form/Form/Action/ConditionalSetValue.php b/framework/Form/Form/Action/ConditionalSetValue.php
new file mode 100644 (file)
index 0000000..40a7e8f
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Horde_Form_Action_ConditionalSetValue is a Horde_Form_Action that
+ * sets the value of one Horde_Form variable based on the value of the
+ * variable the action is attached to.
+ *
+ * $Horde: framework/Form/Form/Action/ConditionalSetValue.php,v 1.4 2009/01/06 17:49:17 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_ConditionalSetValue extends Horde_Form_Action {
+
+    /**
+     * Which JS events should trigger this action?
+     *
+     * @var array
+     */
+    var $_trigger = array('onchange', 'onload');
+
+    function getActionScript($form, $renderer, $varname)
+    {
+        return 'map(\'' . $renderer->_genID($varname, false) . "', '" . $renderer->_genID($this->getTarget(), false) . '\');';
+    }
+
+    function setValues(&$vars, $sourceVal, $arrayVal = false)
+    {
+        $map = $this->_params['map'];
+        $target = $this->getTarget();
+
+        if ($arrayVal) {
+            $i = 0;
+            if (is_array($sourceVal)) {
+                foreach ($sourceVal as $val) {
+                    if (!empty($map[$val])) {
+                        $vars->set($target, $map[$val], $i);
+                    }
+                    $i++;
+                }
+            }
+        } else {
+            if (!empty($map[$sourceVal])) {
+                $vars->set($target, $map[$sourceVal]);
+            }
+        }
+    }
+
+    function printJavaScript()
+    {
+        $this->_printJavaScriptStart();
+        $map = $this->_params['map'];
+?>
+
+var _map = [<?php
+$i = 0;
+foreach ($map as $val) {
+    if ($i > 0) {
+        echo ', ';
+    }
+    echo '"' . $val . '"';
+    $i++;
+}?>];
+
+function map(sourceId, targetId)
+{
+    var newval;
+    var source = document.getElementById(sourceId);
+    var element = document.getElementById(targetId);
+    if (element) {
+        if (_map[source.selectedIndex]) {
+            newval = _map[source.selectedIndex];
+            replace = true;
+        } else {
+            newval = '';
+            replace = false;
+            for (i = 0; i < _map.length; i++) {
+                if (element.value == _map[i]) {
+                    replace = true;
+                    break;
+                }
+            }
+        }
+
+        if (replace) {
+            element.value = newval;
+        }
+    }
+}<?php
+        $this->_printJavaScriptEnd();
+    }
+
+}
diff --git a/framework/Form/Form/Action/conditional_enable.php b/framework/Form/Form/Action/conditional_enable.php
new file mode 100644 (file)
index 0000000..bb6f9d7
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Horde_Form_Action_conditional_enable is a Horde_Form_Action that
+ * enables or disables an element based on the value of another element
+ *
+ * Format of the $params passed to the constructor:
+ * <pre>
+ *  $params = array(
+ *      'target'  => '[name of element this is conditional on]',
+ *      'enabled' => 'true' | 'false',
+ *      'values'  => array([target values to check])
+ *  );
+ * </pre>
+ *
+ * So $params = array('foo', 'true', array(1, 2)) will enable the field this
+ * action is attached to if the value of 'foo' is 1 or 2, and disable it
+ * otherwise.
+ *
+ * $Horde: framework/Form/Form/Action/conditional_enable.php,v 1.13 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Matt Kynaston <matt@kynx.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_conditional_enable extends Horde_Form_Action {
+
+    var $_trigger = array('onload');
+
+    function getActionScript(&$form, $renderer, $varname)
+    {
+        Horde::addScriptFile('form_helpers.js', 'horde');
+
+        $form_name = $form->getName();
+        $target = $this->_params['target'];
+        $enabled = $this->_params['enabled'];
+        if (!is_string($enabled)) {
+            $enabled = ($enabled) ? 'true' : 'false';
+        }
+        $vals = $this->_params['values'];
+        $vals = (is_array($vals)) ? $vals : array($vals);
+        $args = "'$varname', $enabled, '" . implode("','", $vals) . "'";
+
+        return "if (addEvent(document.getElementById('$form_name').$target, 'onchange', \"checkEnabled(this, $args);\")) { "
+            . "  checkEnabled(document.getElementById('$form_name').$varname, $args); };";
+    }
+
+}
diff --git a/framework/Form/Form/Action/conditional_setvalue.php b/framework/Form/Form/Action/conditional_setvalue.php
new file mode 100644 (file)
index 0000000..c08b2c3
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Horde_Form_Action_conditional_setvalue is a Horde_Form_Action that
+ * sets the value of one Horde_Form variable based on the value of the
+ * variable the action is attached to.
+ *
+ * $Horde: framework/Form/Form/Action/conditional_setvalue.php,v 1.28 2009/01/06 17:49:17 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_conditional_setvalue extends Horde_Form_Action {
+
+    /**
+     * Which JS events should trigger this action?
+     *
+     * @var array
+     */
+    var $_trigger = array('onchange', 'onload');
+
+    function getActionScript($form, $renderer, $varname)
+    {
+        return 'map(\'' . $renderer->_genID($varname, false) . "', '" . $renderer->_genID($this->getTarget(), false) . '\');';
+    }
+
+    function setValues(&$vars, $sourceVal, $arrayVal = false)
+    {
+        $map = $this->_params['map'];
+        $target = $this->getTarget();
+
+        if ($arrayVal) {
+            $i = 0;
+            if (is_array($sourceVal)) {
+                foreach ($sourceVal as $val) {
+                    if (!empty($map[$val])) {
+                        $vars->set($target, $map[$val], $i);
+                    }
+                    $i++;
+                }
+            }
+        } else {
+            if (!empty($map[$sourceVal])) {
+                $vars->set($target, $map[$sourceVal]);
+            }
+        }
+    }
+
+    function printJavaScript()
+    {
+        $this->_printJavaScriptStart();
+        $map = $this->_params['map'];
+?>
+
+var _map = [<?php
+$i = 0;
+foreach ($map as $val) {
+    if ($i > 0) {
+        echo ', ';
+    }
+    echo '"' . $val . '"';
+    $i++;
+}?>];
+
+function map(sourceId, targetId)
+{
+    var newval;
+    var source = document.getElementById(sourceId);
+    var element = document.getElementById(targetId);
+    if (element) {
+        if (_map[source.selectedIndex]) {
+            newval = _map[source.selectedIndex];
+            replace = true;
+        } else {
+            newval = '';
+            replace = false;
+            for (i = 0; i < _map.length; i++) {
+                if (element.value == _map[i]) {
+                    replace = true;
+                    break;
+                }
+            }
+        }
+
+        if (replace) {
+            element.value = newval;
+        }
+    }
+}<?php
+        $this->_printJavaScriptEnd();
+    }
+
+}
diff --git a/framework/Form/Form/Action/reload.php b/framework/Form/Form/Action/reload.php
new file mode 100644 (file)
index 0000000..0ede7b9
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Horde_Form_Action_reload is a Horde_Form Action that reloads the
+ * form with the current (not the original) value after the form element
+ * that the action is attached to is modified.
+ *
+ * $Horde: framework/Form/Form/Action/reload.php,v 1.16 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_reload extends Horde_Form_Action {
+
+    var $_trigger = array('onchange');
+
+    function getActionScript($form, $renderer, $varname)
+    {
+        Horde::addScriptFile('prototype.js', 'horde');
+        Horde::addScriptFile('effects.js', 'horde');
+        Horde::addScriptFile('redbox.js', 'horde');
+        return 'if (this.value) { document.' . $form->getName() . '.formname.value=\'\'; RedBox.loading(); document.' . $form->getName() . '.submit() }';
+    }
+
+}
diff --git a/framework/Form/Form/Action/setcursorpos.php b/framework/Form/Form/Action/setcursorpos.php
new file mode 100644 (file)
index 0000000..b3e6333
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Horde_Form_Action_setcursorpos is a Horde_Form_Action that places
+ * the cursor in a text field.
+ *
+ * The params array contains the desired cursor position.
+ *
+ * $Horde: framework/Form/Form/Action/setcursorpos.php,v 1.6 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since Horde 3.2
+ * @package Horde_Form
+ */
+class Horde_Form_Action_setcursorpos extends Horde_Form_Action {
+
+    var $_trigger = array('onload');
+
+    function getActionScript(&$form, $renderer, $varname)
+    {
+        Horde::addScriptFile('form_helpers.js', 'horde');
+
+        $pos = implode(',', $this->_params);
+        return 'form_setCursorPosition(document.forms[\'' .
+            htmlspecialchars($form->getName()) . '\'].elements[\'' .
+            htmlspecialchars($varname) . '\'].id, ' . $pos . ');';
+    }
+
+}
diff --git a/framework/Form/Form/Action/submit.php b/framework/Form/Form/Action/submit.php
new file mode 100644 (file)
index 0000000..06b031c
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Horde_Form_Action_submit is a Horde_Form Action that submits the
+ * form after the form element that the action is attached to is
+ * modified.
+ *
+ * $Horde: framework/Form/Form/Action/submit.php,v 1.19 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_submit extends Horde_Form_Action {
+
+    var $_trigger = array('onchange');
+
+    function getActionScript($form, $renderer, $varname)
+    {
+        Horde::addScriptFile('prototype.js', 'horde');
+        Horde::addScriptFile('effects.js', 'horde');
+        Horde::addScriptFile('redbox.js', 'horde');
+        return 'RedBox.loading(); document.' . $form->getName() . '.submit()';
+    }
+
+}
diff --git a/framework/Form/Form/Action/sum_fields.php b/framework/Form/Form/Action/sum_fields.php
new file mode 100644 (file)
index 0000000..46896c4
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Horde_Form_Action_sum_fields is a Horde_Form_Action that sets the target
+ * field to the sum of one or more other numeric fields.
+ *
+ * The params array should contain the names of the fields which will be
+ * summed.
+ *
+ * $Horde: framework/Form/Form/Action/sum_fields.php,v 1.13 2009/10/06 18:58:57 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Matt Kynaston <matt@kynx.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_sum_fields extends Horde_Form_Action {
+
+    var $_trigger = array('onload');
+
+    function getActionScript(&$form, $renderer, $varname)
+    {
+        Horde::addScriptFile('form_helpers.js', 'horde');
+
+        $form_name = $form->getName();
+        $fields = "'" . implode("','", $this->_params) . "'";
+        $js = array();
+        $js[] = sprintf('document.forms[\'%s\'].elements[\'%s\'].disabled = true;',
+                        $form_name,
+                        $varname);
+        foreach ($this->_params as $field) {
+            $js[] = sprintf("addEvent(document.forms['%1\$s'].elements['%2\$s'], \"onchange\", \"sumFields(document.forms['%1\$s'], '%3\$s', %4\$s);\");",
+                            $form_name,
+                            $field,
+                            $varname,
+                            $fields);
+        }
+
+        return implode("\n", $js);
+    }
+
+}
diff --git a/framework/Form/Form/Action/updatefield.php b/framework/Form/Form/Action/updatefield.php
new file mode 100644 (file)
index 0000000..53b50ec
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Horde_Form_Action_updatefield is a Horde_Form_Action that updates
+ * the value of one Horde_Form variable as the variable the action is
+ * attached to is updated.
+ *
+ * $Horde: framework/Form/Form/Action/updatefield.php,v 1.14 2009/01/06 17:49:17 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Form
+ */
+class Horde_Form_Action_updatefield extends Horde_Form_Action {
+
+    var $_trigger = array('onchange', 'onload', 'onkeyup');
+
+    function getActionScript(&$form, &$renderer, $varname)
+    {
+        return 'updateField' . $this->id() . '();';
+    }
+
+    function setValues(&$vars, $sourceVal, $arrayVal = false)
+    {
+    }
+
+    function printJavaScript()
+    {
+        $this->_printJavaScriptStart();
+        $pieces = explode('%s', $this->_params['format']);
+        $fields = $this->_params['fields'];
+        $val_first = (substr($this->_params['format'], 0, 2) == '%s');
+        if ($val_first) {
+            array_shift($pieces);
+        }
+        if (substr($this->_params['format'], -2) == '%s') {
+            array_pop($pieces);
+        }
+
+        $args = array();
+        if ($val_first) {
+            $args[] = "document.getElementById('" . array_shift($fields) . "').value";
+        }
+        while (count($pieces)) {
+            $args[] = "'" . array_shift($pieces) . "'";
+            $args[] = "document.getElementById('" . array_shift($fields) . "').value";
+        }
+?>
+// Updater for <?php echo $this->getTarget() ?>.
+function updateField<?php echo $this->id() ?>()
+{
+    var target = document.getElementById('<?php echo $this->getTarget() ?>');
+    if (target) {
+        target.value = (<?php echo implode(' + ', str_replace("\n", "\\n", $args)) ?>).replace(/(^ +| +$)/, '').replace(/ +/g, ' ');
+    }
+}<?php
+        $this->_printJavaScriptEnd();
+    }
+
+}
diff --git a/framework/Form/Form/Renderer.php b/framework/Form/Form/Renderer.php
new file mode 100644 (file)
index 0000000..ce075d1
--- /dev/null
@@ -0,0 +1,540 @@
+<?php
+/**
+ * The Horde_Form_Renderer class provides HTML and other renderings of
+ * forms for the Horde_Form:: package.
+ *
+ * $Horde: framework/Form/Form/Renderer.php,v 1.228 2009-11-21 16:54:37 chuck Exp $
+ *
+ * Copyright 2001-2007 Robert E. Coyle <robertecoyle@hotmail.com>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Robert E. Coyle <robertecoyle@hotmail.com>
+ * @package Horde_Form
+ */
+class Horde_Form_Renderer {
+
+    var $_name;
+    var $_requiredLegend = false;
+    var $_requiredMarker = '*';
+    var $_helpMarker = '?';
+    var $_showHeader = true;
+    var $_cols = 2;
+    var $_varRenderer = null;
+    var $_firstField = null;
+    var $_stripedRows = true;
+
+    /**
+     * Does the title of the form contain HTML? If so, you are responsible for
+     * doing any needed escaping/sanitization yourself. Otherwise the title
+     * will be run through htmlspecialchars() before being output.
+     *
+     * @var boolean
+     */
+    var $_encodeTitle = true;
+
+    /**
+     * Width of the attributes column.
+     *
+     * @access private
+     * @var string
+     */
+    var $_attrColumnWidth = '15%';
+
+    /**
+     * Construct a new Horde_Form_Renderer::.
+     *
+     * @param array $params  This is a hash of renderer-specific parameters.
+     *                       Possible keys:<code>
+     *                       'varrenderer_driver': specifies the driver
+     *                           parameter to Horde_Ui_VarRenderer::factory().
+     *                       'encode_title': @see $_encodeTitle</code>
+     */
+    function Horde_Form_Renderer($params = array())
+    {
+        global $registry;
+        if (isset($registry) && is_a($registry, 'Registry')) {
+            /* Registry available, so use a pretty image. */
+            $this->_requiredMarker = Horde::img('required.png', '*', '', $registry->getImageDir('horde'));
+        } else {
+            /* No registry available, use something plain. */
+            $this->_requiredMarker = '*';
+        }
+
+        if (isset($params['encode_title'])) {
+            $this->encodeTitle($params['encode_title']);
+        }
+
+        $driver = 'html';
+        if (isset($params['varrenderer_driver'])) {
+            $driver = $params['varrenderer_driver'];
+        }
+        $this->_varRenderer = Horde_Ui_VarRenderer::factory($driver, $params);
+    }
+
+    function showHeader($bool)
+    {
+        $this->_showHeader = $bool;
+    }
+
+    /**
+     * Sets or returns whether the form title should be encoded with
+     * htmlspecialchars().
+     *
+     * @param boolean $encode  If true, the form title gets encoded.  If false
+     *                         the title can contain HTML, but the class user
+     *                         is responsible to encode any special characters.
+     *
+     * @return boolean  Whether the form title should be encoded.
+     */
+    function encodeTitle($encode = null)
+    {
+        if (!is_null($encode)) {
+            $this->_encodeTitle = $encode;
+        }
+        return $this->_encodeTitle = $encode;
+    }
+
+    /**
+     * @deprecated
+     */
+    function setAttrColumnWidth($width)
+    {
+    }
+
+    function open($action, $method, $name, $enctype = null)
+    {
+        $this->_name = $name;
+        $name = htmlspecialchars($name);
+        $action = htmlspecialchars($action);
+        $method = htmlspecialchars($method);
+        echo "<form action=\"$action\" method=\"$method\"" . (empty($name) ? '' : " name=\"$name\" id=\"$name\"") . (is_null($enctype) ? '' : " enctype=\"$enctype\"") . ">\n";
+        Horde_Util::pformInput();
+    }
+
+    function beginActive($name, $extra = null)
+    {
+        $this->_renderBeginActive($name, $extra);
+    }
+
+    function beginInactive($name, $extra = null)
+    {
+        $this->_renderBeginInactive($name, $extra);
+    }
+
+    function _renderSectionTabs(&$form)
+    {
+        /* If javascript is not available, do not render tabs. */
+        if (!$GLOBALS['browser']->hasFeature('javascript')) {
+            return;
+        }
+
+        $open_section = $form->getOpenSection();
+
+        /* Add the javascript for the toggling the sections. */
+        Horde::addScriptFile('form_sections.js', 'horde');
+        echo '<script type="text/javascript">' . "\n";
+        printf('var sections_%1$s = new Horde_Form_Sections(\'%1$s\', \'%2$s\');',
+               $form->getName(),
+               $open_section);
+        echo "\n" . '</script>';
+
+        /* Loop through the sections and print out a tab for each. */
+        echo "<div class=\"tabset\"><ul>\n";
+        foreach ($form->_sections as $section => $val) {
+            $class = ($section == $open_section) ? ' class="activeTab"' : '';
+            $js = sprintf('onclick="sections_%s.toggle(\'%s\'); return false;"',
+                          $form->getName(),
+                          $section);
+            printf('<li%s id="%s"><a href="#" %s>%s%s</a> </li>' . "\n",
+                   $class, htmlspecialchars($form->getName() . '_tab_' . $section), $js,
+                   $form->getSectionImage($section),
+                   $form->getSectionDesc($section));
+        }
+        echo "</ul></div><br class=\"clear\" />\n";
+    }
+
+    function _renderSectionBegin(&$form, $section)
+    {
+        // Stripe alternate rows if that option is turned on.
+        if ($this->_stripedRows && class_exists('Horde')) {
+            Horde::addScriptFile('stripe.js', 'horde');
+            $class = ' class="striped"';
+        } else {
+            $class = '';
+        }
+
+        $open_section = $form->getOpenSection();
+        if (empty($open_section)) {
+            $open_section = '__base';
+        }
+        printf('<div id="%s" style="display:%s;"><table%s cellspacing="0">',
+               htmlspecialchars($form->getName() . '_section_' . $section),
+               ($open_section == $section ? 'block' : 'none'),
+               $class);
+    }
+
+    function _renderSectionEnd()
+    {
+        echo '</table></div>';
+    }
+
+    function end()
+    {
+        $this->_renderEnd();
+    }
+
+    function close($focus = true)
+    {
+        echo "</form>\n";
+        if ($focus && !empty($this->_firstField)) {
+            echo '<script type="text/javascript">
+<!--
+try {
+    document.getElementById("' . $this->_firstField . '").focus();
+} catch(e) {}
+//-->
+</script>
+';
+        }
+    }
+
+    function listFormVars(&$form)
+    {
+        $variables = &$form->getVariables(true, true);
+        $vars = array();
+        if ($variables) {
+            foreach ($variables as $var) {
+                if (is_object($var)) {
+                    if (!$var->isReadonly()) {
+                        $vars[$var->getVarName()] = 1;
+                    }
+                } else {
+                    $vars[$var] = 1;
+                }
+            }
+        }
+        echo '<input type="hidden" name="_formvars" value="' . @htmlspecialchars(serialize($vars), ENT_QUOTES, Horde_Nls::getCharset()) . '" />';
+    }
+
+    function renderFormActive(&$form, &$vars)
+    {
+        $this->_renderForm($form, $vars, true);
+    }
+
+    function renderFormInactive(&$form, &$vars)
+    {
+        $this->_renderForm($form, $vars, false);
+    }
+
+    function _renderForm(&$form, &$vars, $active)
+    {
+        /* If help is present 3 columns are needed. */
+        $this->_cols = $form->hasHelp() ? 3 : 2;
+
+        $variables = &$form->getVariables(false);
+
+        /* Check for a form token error. */
+        if (($tokenError = $form->getError('_formToken')) !== null) {
+            echo '<p class="form-error">' . htmlspecialchars($tokenError) . '</p>';
+        }
+
+        /* Check for a form secret error. */
+        if (($secretError = $form->getError('_formSecret')) !== null) {
+            echo '<p class="form-error">' . htmlspecialchars($secretError) . '</p>';
+        }
+
+        if (count($form->_sections)) {
+            $this->_renderSectionTabs($form);
+        }
+
+        $error_section = null;
+        foreach ($variables as $section_id => $section) {
+            $this->_renderSectionBegin($form, $section_id);
+            foreach ($section as $var) {
+                $type = $var->getTypeName();
+
+                switch ($type) {
+                case 'header':
+                    $this->_renderHeader($var->getHumanName(), $form->getError($var->getVarName()));
+                    break;
+
+                case 'description':
+                    $this->_renderDescription($var->getHumanName());
+                    break;
+
+                case 'spacer':
+                    $this->_renderSpacer();
+                    break;
+
+                default:
+                    $isInput = ($active && !$var->isReadonly());
+                    $format = $isInput ? 'Input' : 'Display';
+                    $begin = "_renderVar${format}Begin";
+                    $end = "_renderVar${format}End";
+
+                    $this->$begin($form, $var, $vars);
+                    echo $this->_varRenderer->render($form, $var, $vars, $isInput);
+                    $this->$end($form, $var, $vars);
+
+                    /* Print any javascript if actions present. */
+                    if ($var->hasAction()) {
+                        $var->_action->printJavaScript();
+                    }
+
+                    /* Keep first field. */
+                    if ($active && empty($this->_firstField) && !$var->isReadonly() && !$var->isHidden()) {
+                        $this->_firstField = $var->getVarName();
+                    }
+
+                    /* Keep section with first error. */
+                    if (is_null($error_section) && $form->getError($var)) {
+                        $error_section = $section_id;
+                    }
+                }
+            }
+
+            $this->_renderSectionEnd();
+        }
+
+        if (!is_null($error_section) && $form->_sections) {
+            echo '<script type="text/javascript">' .
+                "\n" . sprintf('sections_%s.toggle(\'%s\');',
+                               $form->getName(),
+                               $error_section) .
+                "\n</script>";
+        }
+    }
+
+    function submit($submit = null, $reset = false)
+    {
+        if (is_null($submit) || empty($submit)) {
+            $submit = _("Submit");
+        }
+        if ($reset === true) {
+            $reset = _("Reset");
+        }
+        $this->_renderSubmit($submit, $reset);
+    }
+
+    /**
+     * Implementation specific begin function.
+     */
+    function _renderBeginActive($name, $extra)
+    {
+        echo '<div class="form" id="' . htmlspecialchars($this->_name) . '_active">';
+        if ($this->_showHeader) {
+            $this->_sectionHeader($name, $extra);
+        }
+        if ($this->_requiredLegend) {
+            echo '<span class="form-error">' . $this->_requiredMarker . '</span> = ' . _("Required Field");
+        }
+    }
+
+    /**
+     * Implementation specific begin function.
+     */
+    function _renderBeginInactive($name, $extra)
+    {
+        echo '<div class="form" id="' . htmlspecialchars($this->_name) . '_inactive">';
+        if ($this->_showHeader) {
+            $this->_sectionHeader($name, $extra);
+        }
+    }
+
+    /**
+     * Implementation specific end function.
+     */
+    function _renderEnd()
+    {
+        echo '</div>' . $this->_varRenderer->renderEnd();
+    }
+
+    function _renderHeader($header, $error = '')
+    {
+?><tr><td class="control" width="100%" colspan="<?php echo $this->_cols ?>" valign="bottom"><strong><?php echo $header ?></strong><?php
+        if (!empty($error)) {
+?><br /><span class="form-error"><?php echo $error ?></span><?php
+        }
+?></td></tr>
+<?php
+    }
+
+    function _renderDescription($text)
+    {
+?><tr><td width="100%" colspan="<?php echo $this->_cols ?>"><p style="padding:8px"><?php echo $text ?></p></td></tr>
+<?php
+    }
+
+    function _renderSpacer()
+    {
+?><tr><td colspan="<?php echo $this->_cols ?>">&nbsp;</td></tr>
+<?php
+    }
+
+    function _renderSubmit($submit, $reset)
+    {
+?><div class="control">
+  <?php if (!is_array($submit)) $submit = array($submit); foreach ($submit as $submitbutton): ?>
+    <input class="button" name="submitbutton" type="submit" value="<?php echo $submitbutton ?>" />
+  <?php endforeach; ?>
+  <?php if (!empty($reset)): ?>
+    <input class="button" name="resetbutton" type="reset" value="<?php echo $reset ?>" />
+  <?php endif; ?>
+</div>
+<?php
+    }
+
+    // Implementation specifics -- input variables.
+    function _renderVarInputBegin(&$form, &$var, &$vars)
+    {
+        $message = $form->getError($var);
+        $isvalid = empty($message);
+        echo "<tr valign=\"top\">\n";
+        printf('  <td%s align="right">%s%s%s%s</td>' . "\n",
+               empty($this->_attrColumnWidth) ? '' : ' width="' . $this->_attrColumnWidth . '"',
+               $isvalid ? '' : '<span class="form-error">',
+               $var->isRequired() ? '<span class="form-error">' . $this->_requiredMarker . '</span>&nbsp;' : '',
+               $var->getHumanName(),
+               $isvalid ? '' : '<br />' . $message . '</span>');
+        printf('  <td%s%s>',
+               ((!$var->hasHelp() && $form->hasHelp()) ? ' colspan="2"' : ''),
+               ($var->isDisabled() ? ' class="form-disabled"' : ''));
+    }
+
+    function _renderVarInputEnd(&$form, &$var, &$vars)
+    {
+        /* Display any description for the field. */
+        if ($var->hasDescription()) {
+            echo '<br />' . $var->getDescription();
+        }
+
+        /* Display any help for the field. */
+        if ($var->hasHelp()) {
+            global $registry;
+            if (isset($registry) && is_a($registry, 'Registry')) {
+                $link = Horde_Help::link($GLOBALS['registry']->getApp(), $var->getHelp());
+            } else {
+                $link = '<a href="#" onclick="alert(\'' . addslashes(@htmlspecialchars($var->getHelp())) . '\');return false;">' . $this->_helpMarker . '</a>';
+            }
+            echo "</td>\n  <td style=\"text-align:right\">$link&nbsp;";
+        }
+
+        echo "</td>\n</tr>\n";
+    }
+
+    // Implementation specifics -- display variables.
+    function _renderVarDisplayBegin(&$form, &$var, &$vars)
+    {
+        $message = $form->getError($var);
+        $isvalid = empty($message);
+        echo "<tr valign=\"top\">\n";
+        printf('  <td%s align="right">%s<strong>%s</strong>%s</td>' . "\n",
+               empty($this->_attrColumnWidth) ? '' : ' width="' . $this->_attrColumnWidth . '"',
+               $isvalid ? '' : '<span class="form-error">',
+               $var->getHumanName(),
+               $isvalid ? '' : '<br />' . $message . '</span>');
+        echo '  <td>';
+    }
+
+    function _renderVarDisplayEnd(&$form, &$var, &$vars)
+    {
+        if ($var->hasHelp()) {
+            echo '</td><td>&nbsp;';
+        }
+        echo "</td>\n</tr>\n";
+    }
+
+    function _sectionHeader($title, $extra = '')
+    {
+        if (strlen($title)) {
+            echo '<div class="header">';
+            if (!empty($extra)) {
+                echo '<span class="rightFloat">' . $extra . '</span>';
+            }
+            echo $this->_encodeTitle ? htmlspecialchars($title) : $title;
+            echo '</div>';
+        }
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Form_Renderer instance based on
+     * $renderer.
+     *
+     * @param mixed $renderer  The type of concrete Horde_Form_Renderer
+     *                         subclass to return. The code is dynamically
+     *                         included. If $renderer is an array, then we will
+     *                         look in $renderer[0]/lib/Form/Renderer/ for the
+     *                         subclass implementation named $renderer[1].php.
+     * @param array $params    A hash containing any additional configuration a
+     *                         form might need.
+     *
+     * @return Horde_Form_Renderer  The concrete Horde_Form_Renderer reference,
+     *                              or false on an error.
+     */
+    function factory($renderer = '', $params = null)
+    {
+        if (is_array($renderer)) {
+            $app = $renderer[0];
+            $renderer = $renderer[1];
+        }
+
+        /* Return a base Horde_Form_Renderer object if no driver is
+         * specified. */
+        $renderer = basename($renderer);
+        if (!empty($renderer) && $renderer != 'none') {
+            $class = 'Horde_Form_Renderer_' . $renderer;
+        } else {
+            $class = 'Horde_Form_Renderer';
+        }
+
+        if (!class_exists($class)) {
+            if (!empty($app)) {
+                include $GLOBALS['registry']->get('fileroot', $app) . '/lib/Form/Renderer/' . $renderer . '.php';
+            } else {
+                include 'Horde/Form/Renderer/' . $renderer . '.php';
+            }
+        }
+
+        if (class_exists($class)) {
+            return new $class($params);
+        } else {
+            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+    }
+
+    /**
+     * Attempts to return a reference to a concrete Horde_Form_Renderer
+     * instance based on $renderer. It will only create a new instance if no
+     * Horde_Form_Renderer instance with the same parameters currently exists.
+     *
+     * This should be used if multiple types of form renderers (and,
+     * thus, multiple Horde_Form_Renderer instances) are required.
+     *
+     * This method must be invoked as: $var = &Horde_Form_Renderer::singleton()
+     *
+     * @param mixed $renderer  The type of concrete Horde_Form_Renderer
+     *                         subclass to return. The code is dynamically
+     *                         included. If $renderer is an array, then we will
+     *                         look in $renderer[0]/lib/Form/Renderer/ for the
+     *                         subclass implementation named $renderer[1].php.
+     * @param array $params  A hash containing any additional configuration a
+     *                       form might need.
+     *
+     * @return Horde_Form_Renderer  The concrete Horde_Form_Renderer reference,
+     *                              or false on an error.
+     */
+    function &singleton($renderer, $params = null)
+    {
+        static $instances = array();
+
+        $signature = serialize(array($renderer, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = Horde_Form_Renderer::factory($renderer, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Form/Form/Type.php b/framework/Form/Form/Type.php
new file mode 100644 (file)
index 0000000..5cc7eae
--- /dev/null
@@ -0,0 +1,3553 @@
+<?php
+/**
+ * Horde_Form_Type Class
+ *
+ * @author  Robert E. Coyle <robertecoyle@hotmail.com>
+ * @package Horde_Form
+ */
+class Horde_Form_Type {
+
+    function Horde_Form_Type()
+    {
+    }
+
+    function getProperty($property)
+    {
+        $prop = '_' . $property;
+        return isset($this->$prop) ? $this->$prop : null;
+    }
+
+    function __get($property)
+    {
+        return $this->getProperty($property);
+    }
+
+    function setProperty($property, $value)
+    {
+        $prop = '_' . $property;
+        $this->$prop = $value;
+    }
+
+    function __set($property, $value)
+    {
+        return $this->setProperty($property, $value);
+    }
+
+    function init()
+    {
+    }
+
+    function onSubmit()
+    {
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $message = '<strong>Error:</strong> Horde_Form_Type::isValid() called - should be overridden<br />';
+        return false;
+    }
+
+    function getTypeName()
+    {
+        return str_replace('horde_form_type_', '', Horde_String::lower(get_class($this)));
+    }
+
+    function getValues()
+    {
+        return null;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = $var->getValue($vars);
+    }
+
+}
+
+class Horde_Form_Type_spacer extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Spacer"));
+    }
+
+}
+
+class Horde_Form_Type_header extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Header"));
+    }
+
+}
+
+class Horde_Form_Type_description extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Description"));
+    }
+
+}
+
+/**
+ * Simply renders its raw value in both active and inactive rendering.
+ */
+class Horde_Form_Type_html extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("HTML"));
+    }
+
+}
+
+class Horde_Form_Type_number extends Horde_Form_Type {
+
+    var $_fraction;
+
+    function init($fraction = null)
+    {
+        $this->_fraction = $fraction;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) {
+            $message = _("This field is required.");
+            return false;
+        } elseif (empty($value)) {
+            return true;
+        }
+
+        /* If matched, then this is a correct numeric value. */
+        if (preg_match($this->_getValidationPattern(), $value)) {
+            return true;
+        }
+
+        $message = _("This field must be a valid number.");
+        return false;
+    }
+
+    function _getValidationPattern()
+    {
+        static $pattern = '';
+        if (!empty($pattern)) {
+            return $pattern;
+        }
+
+        /* Get current locale information. */
+        $linfo = Horde_Nls::getLocaleInfo();
+
+        /* Build the pattern. */
+        $pattern = '(-)?';
+
+        /* Only check thousands separators if locale has any. */
+        if (!empty($linfo['mon_thousands_sep'])) {
+            /* Regex to check for correct thousands separators (if any). */
+            $pattern .= '((\d+)|((\d{0,3}?)([' . $linfo['mon_thousands_sep'] . ']\d{3})*?))';
+        } else {
+            /* No locale thousands separator, check for only digits. */
+            $pattern .= '(\d+)';
+        }
+        /* If no decimal point specified default to dot. */
+        if (empty($linfo['mon_decimal_point'])) {
+            $linfo['mon_decimal_point'] = '.';
+        }
+        /* Regex to check for correct decimals (if any). */
+        if (empty($this->_fraction)) {
+            $fraction = '*';
+        } else {
+            $fraction = '{0,' . $this->_fraction . '}';
+        }
+        $pattern .= '([' . $linfo['mon_decimal_point'] . '](\d' . $fraction . '))?';
+
+        /* Put together the whole regex pattern. */
+        $pattern = '/^' . $pattern . '$/';
+
+        return $pattern;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $value = $vars->get($var->getVarName());
+        $linfo = Horde_Nls::getLocaleInfo();
+        $value = str_replace($linfo['mon_thousands_sep'], '', $value);
+        $info = str_replace($linfo['mon_decimal_point'], '.', $value);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Number"));
+    }
+
+}
+
+class Horde_Form_Type_int extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || preg_match('/^[0-9]+$/', $value)) {
+            return true;
+        }
+
+        $message = _("This field may only contain integers.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Integer"));
+    }
+
+}
+
+class Horde_Form_Type_octal extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || preg_match('/^[0-7]+$/', $value)) {
+            return true;
+        }
+
+        $message = _("This field may only contain octal values.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Octal"));
+    }
+
+}
+
+class Horde_Form_Type_intlist extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (empty($value) && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || preg_match('/^[0-9 ,]+$/', $value)) {
+            return true;
+        }
+
+        $message = _("This field must be a comma or space separated list of integers");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Integer list"));
+    }
+
+}
+
+class Horde_Form_Type_text extends Horde_Form_Type {
+
+    var $_regex;
+    var $_size;
+    var $_maxlength;
+
+    /**
+     * The initialisation function for the text variable type.
+     *
+     * @access private
+     *
+     * @param string $regex       Any valid PHP PCRE pattern syntax that
+     *                            needs to be matched for the field to be
+     *                            considered valid. If left empty validity
+     *                            will be checked only for required fields
+     *                            whether they are empty or not.
+     *                            If using this regex test it is advisable
+     *                            to enter a description for this field to
+     *                            warn the user what is expected, as the
+     *                            generated error message is quite generic
+     *                            and will not give any indication where
+     *                            the regex failed.
+     * @param integer $size       The size of the input field.
+     * @param integer $maxlength  The max number of characters.
+     */
+    function init($regex = '', $size = 40, $maxlength = null)
+    {
+        $this->_regex     = $regex;
+        $this->_size      = $size;
+        $this->_maxlength = $maxlength;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $valid = true;
+
+        if (!empty($this->_maxlength) && Horde_String::length($value) > $this->_maxlength) {
+            $valid = false;
+            $message = sprintf(_("Value is over the maximum length of %d."), $this->_maxlength);
+        } elseif ($var->isRequired() && empty($this->_regex)) {
+            $valid = strlen(trim($value)) > 0;
+
+            if (!$valid) {
+                $message = _("This field is required.");
+            }
+        } elseif (!empty($this->_regex)) {
+            $valid = preg_match($this->_regex, $value);
+
+            if (!$valid) {
+                $message = _("You must enter a valid value.");
+            }
+        }
+
+        return $valid;
+    }
+
+    function getSize()
+    {
+        return $this->_size;
+    }
+
+    function getMaxLength()
+    {
+        return $this->_maxlength;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Text"),
+            'params' => array(
+                'regex'     => array('label' => _("Regex"),
+                                     'type'  => 'text'),
+                'size'      => array('label' => _("Size"),
+                                     'type'  => 'int'),
+                'maxlength' => array('label' => _("Maximum length"),
+                                     'type'  => 'int')));
+    }
+
+}
+
+class Horde_Form_Type_stringlist extends Horde_Form_Type_text {
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("String list"),
+            'params' => array(
+                'regex'     => array('label' => _("Regex"),
+                                     'type'  => 'text'),
+                'size'      => array('label' => _("Size"),
+                                     'type'  => 'int'),
+                'maxlength' => array('label' => _("Maximum length"),
+                                     'type'  => 'int')),
+        );
+    }
+
+}
+
+/**
+ * @since Horde 3.3
+ */
+class Horde_Form_Type_stringarray extends Horde_Form_Type_stringlist {
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = array_map('trim', explode(',', $vars->get($var->getVarName())));
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("String list returning an array"),
+            'params' => array(
+                'regex'     => array('label' => _("Regex"),
+                                     'type'  => 'text'),
+                'size'      => array('label' => _("Size"),
+                                     'type'  => 'int'),
+                'maxlength' => array('label' => _("Maximum length"),
+                                     'type'  => 'int')),
+        );
+    }
+
+}
+
+/**
+ * @since Horde 3.2
+ */
+class Horde_Form_Type_phone extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (!strlen(trim($value))) {
+            if ($var->isRequired()) {
+                $message = _("This field is required.");
+                return false;
+            }
+        } elseif (!preg_match('/^\+?[\d()\-\/. ]*$/', $value)) {
+            $message = _("You must enter a valid phone number, digits only with an optional '+' for the international dialing prefix.");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Phone number"));
+    }
+
+}
+
+class Horde_Form_Type_cellphone extends Horde_Form_Type_phone {
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Mobile phone number"));
+    }
+
+}
+
+class Horde_Form_Type_ipaddress extends Horde_Form_Type_text {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $valid = true;
+
+        if (strlen(trim($value)) > 0) {
+            $ip = explode('.', $value);
+            $valid = (count($ip) == 4);
+            if ($valid) {
+                foreach ($ip as $part) {
+                    if (!is_numeric($part) ||
+                        $part > 255 ||
+                        $part < 0) {
+                        $valid = false;
+                        break;
+                    }
+                }
+            }
+
+            if (!$valid) {
+                $message = _("Please enter a valid IP address.");
+            }
+        } elseif ($var->isRequired()) {
+            $valid = false;
+            $message = _("This field is required.");
+        }
+
+        return $valid;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("IP address"));
+    }
+
+}
+
+class Horde_Form_Type_longtext extends Horde_Form_Type_text {
+
+    var $_rows;
+    var $_cols;
+    var $_helper = array();
+
+    function init($rows = 8, $cols = 80, $helper = array())
+    {
+        if (!is_array($helper)) {
+            $helper = array($helper);
+        }
+
+        $this->_rows = $rows;
+        $this->_cols = $cols;
+        $this->_helper = $helper;
+    }
+
+    function getRows()
+    {
+        return $this->_rows;
+    }
+
+    function getCols()
+    {
+        return $this->_cols;
+    }
+
+    function hasHelper($option = '')
+    {
+        if (empty($option)) {
+            /* No option specified, check if any helpers have been
+             * activated. */
+            return !empty($this->_helper);
+        } elseif (empty($this->_helper)) {
+            /* No helpers activated at all, return false. */
+            return false;
+        } else {
+            /* Check if given helper has been activated. */
+            return in_array($option, $this->_helper);
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Long text"),
+            'params' => array(
+                'rows'   => array('label' => _("Number of rows"),
+                                  'type'  => 'int'),
+                'cols'   => array('label' => _("Number of columns"),
+                                  'type'  => 'int'),
+                'helper' => array('label' => _("Helpers"),
+                                  'type'  => 'array')));
+    }
+
+}
+
+class Horde_Form_Type_countedtext extends Horde_Form_Type_longtext {
+
+    var $_chars;
+
+    function init($rows = null, $cols = null, $chars = 1000)
+    {
+        parent::init($rows, $cols);
+        $this->_chars = $chars;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $valid = true;
+
+        $length = Horde_String::length(trim($value));
+
+        if ($var->isRequired() && $length <= 0) {
+            $valid = false;
+            $message = _("This field is required.");
+        } elseif ($length > $this->_chars) {
+            $valid = false;
+            $message = sprintf(ngettext("There are too many characters in this field. You have entered %d character; ", "There are too many characters in this field. You have entered %d characters; ", $length), $length)
+                . sprintf(_("you must enter less than %d."), $this->_chars);
+        }
+
+        return $valid;
+    }
+
+    function getChars()
+    {
+        return $this->_chars;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Counted text"),
+            'params' => array(
+                'rows'  => array('label' => _("Number of rows"),
+                                 'type'  => 'int'),
+                'cols'  => array('label' => _("Number of columns"),
+                                 'type'  => 'int'),
+                'chars' => array('label' => _("Number of characters"),
+                                 'type'  => 'int')));
+    }
+
+}
+
+class Horde_Form_Type_address extends Horde_Form_Type_longtext {
+
+    function parse($address)
+    {
+        $info = array();
+        $aus_state_regex = '(?:ACT|NSW|NT|QLD|SA|TAS|VIC|WA)';
+
+        if (preg_match('/(?s)(.*?)(?-s)\r?\n(?:(.*?)\s+)?((?:A[BL]|B[ABDHLNRST]?|C[ABFHMORTVW]|D[ADEGHLNTY]|E[CHNX]?|F[KY]|G[LUY]?|H[ADGPRSUX]|I[GMPV]|JE|K[ATWY]|L[ADELNSU]?|M[EKL]?|N[EGNPRW]?|O[LX]|P[AEHLOR]|R[GHM]|S[AEGKLMNOPRSTWY]?|T[ADFNQRSW]|UB|W[ACDFNRSV]?|YO|ZE)\d(?:\d|[A-Z])? \d[A-Z]{2})/', $address, $addressParts)) {
+            /* UK postcode detected. */
+            $info = array('country' => 'uk', 'zip' => $addressParts[3]);
+            if (!empty($addressParts[1])) {
+                $info['street'] = $addressParts[1];
+            }
+            if (!empty($addressParts[2])) {
+                $info['city'] = $addressParts[2];
+            }
+        } elseif (preg_match('/\b' . $aus_state_regex . '\b/', $address)) {
+            /* Australian state detected. */
+            /* Split out the address, line-by-line. */
+            $addressLines = preg_split('/\r?\n/', $address);
+            $info = array('country' => 'au');
+            for ($i = 0; $i < count($addressLines); $i++) {
+                /* See if it's the street number & name. */
+                if (preg_match('/(\d+\s*\/\s*)?(\d+|\d+[a-zA-Z])\s+([a-zA-Z ]*)/', $addressLines[$i], $lineParts)) {
+                    $info['street'] = $addressLines[$i];
+                    $info['streetNumber'] = $lineParts[2];
+                    $info['streetName'] = $lineParts[3];
+                }
+                /* Look for "Suburb, State". */
+                if (preg_match('/([a-zA-Z ]*),?\s+(' . $aus_state_regex . ')/', $addressLines[$i], $lineParts)) {
+                    $info['city'] = $lineParts[1];
+                    $info['state'] = $lineParts[2];
+                }
+                /* Look for "State <4 digit postcode>". */
+                if (preg_match('/(' . $aus_state_regex . ')\s+(\d{4})/', $addressLines[$i], $lineParts)) {
+                    $info['state'] = $lineParts[1];
+                    $info['zip'] = $lineParts[2];
+                }
+            }
+        } elseif (preg_match('/(?s)(.*?)(?-s)\r?\n(.*)\s*,\s*(\w+)\.?\s+(\d+|[a-zA-Z]\d[a-zA-Z]\s?\d[a-zA-Z]\d)/', $address, $addressParts)) {
+            /* American/Canadian address style. */
+            $info = array('country' => 'us');
+            if (!empty($addressParts[4]) &&
+                preg_match('|[a-zA-Z]\d[a-zA-Z]\s?\d[a-zA-Z]\d|', $addressParts[4])) {
+                $info['country'] = 'ca';
+            }
+            if (!empty($addressParts[1])) {
+                $info['street'] = $addressParts[1];
+            }
+            if (!empty($addressParts[2])) {
+                $info['city'] = $addressParts[2];
+            }
+            if (!empty($addressParts[3])) {
+                $info['state'] = $addressParts[3];
+            }
+            if (!empty($addressParts[4])) {
+                $info['zip'] = $addressParts[4];
+            }
+        } elseif (preg_match('/(?:(?s)(.*?)(?-s)(?:\r?\n|,\s*))?(?:([A-Z]{1,3})-)?(\d{4,5})\s+(.*)(?:\r?\n(.*))?/i', $address, $addressParts)) {
+            /* European address style. */
+            $info = array();
+            if (!empty($addressParts[1])) {
+                $info['street'] = $addressParts[1];
+            }
+            if (!empty($addressParts[2])) {
+                include 'Horde/NLS/carsigns.php';
+                $country = array_search(Horde_String::upper($addressParts[2]), $carsigns);
+                if ($country) {
+                    $info['country'] = $country;
+                }
+            }
+            if (!empty($addressParts[5])) {
+                include 'Horde/NLS/countries.php';
+                $country = array_search($addressParts[5], $countries);
+                if ($country) {
+                    $info['country'] = Horde_String::lower($country);
+                } elseif (!isset($info['street'])) {
+                    $info['street'] = trim($addressParts[5]);
+                } else {
+                    $info['street'] .= "\n" . $addressParts[5];
+                }
+            }
+            if (!empty($addressParts[3])) {
+                $info['zip'] = $addressParts[3];
+            }
+            if (!empty($addressParts[4])) {
+                $info['city'] = trim($addressParts[4]);
+            }
+        }
+
+        return $info;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Address"),
+            'params' => array(
+                'rows' => array('label' => _("Number of rows"),
+                                'type'  => 'int'),
+                'cols' => array('label' => _("Number of columns"),
+                                'type'  => 'int')));
+    }
+
+}
+
+class Horde_Form_Type_addresslink extends Horde_Form_Type_address {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Address Link"));
+    }
+
+}
+
+/**
+ * @since Horde 3.3
+ */
+class Horde_Form_Type_pgp extends Horde_Form_Type_longtext {
+
+    /**
+     * Path to the GnuPG binary.
+     *
+     * @var string
+     */
+    var $_gpg;
+
+    /**
+     * A temporary directory.
+     *
+     * @var string
+     */
+    var $_temp;
+
+    function init($gpg, $temp_dir = null, $rows = null, $cols = null)
+    {
+        $this->_gpg = $gpg;
+        $this->_temp = $temp_dir;
+        parent::init($rows, $cols);
+    }
+
+    /**
+     * Returns a parameter hash for the Horde_Crypt_pgp constructor.
+     *
+     * @return array  A parameter hash.
+     */
+    function getPGPParams()
+    {
+        return array('program' => $this->_gpg, 'temp' => $this->_temp);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("PGP Key"),
+            'params' => array(
+                'gpg'      => array('label' => _("Path to the GnuPG binary"),
+                                    'type'  => 'string'),
+                'temp_dir' => array('label' => _("A temporary directory"),
+                                    'type'  => 'string'),
+                'rows'     => array('label' => _("Number of rows"),
+                                    'type'  => 'int'),
+                'cols'     => array('label' => _("Number of columns"),
+                                    'type'  => 'int')));
+    }
+
+}
+
+/**
+ * @since Horde 3.3
+ */
+class Horde_Form_Type_smime extends Horde_Form_Type_longtext {
+
+    /**
+     * A temporary directory.
+     *
+     * @var string
+     */
+    var $_temp;
+
+    function init($temp_dir = null, $rows = null, $cols = null)
+    {
+        $this->_temp = $temp_dir;
+        parent::init($rows, $cols);
+    }
+
+    /**
+     * Returns a parameter hash for the Horde_Crypt_smime constructor.
+     *
+     * @return array  A parameter hash.
+     */
+    function getSMIMEParams()
+    {
+        return array('temp' => $this->_temp);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("S/MIME Key"),
+            'params' => array(
+                'temp_dir' => array('label' => _("A temporary directory"),
+                                    'type'  => 'string'),
+                'rows'     => array('label' => _("Number of rows"),
+                                    'type'  => 'int'),
+                'cols'     => array('label' => _("Number of columns"),
+                                    'type'  => 'int')));
+    }
+
+}
+
+/**
+ * @since Horde 3.2
+ */
+class Horde_Form_Type_country extends Horde_Form_Type_enum {
+
+    function init($prompt = null)
+    {
+        parent::init(Horde_Nls::getCountryISO(), $prompt);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Country drop down list"),
+            'params' => array(
+                'prompt' => array('label' => _("Prompt text"),
+                                  'type'  => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_file extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired()) {
+            $uploaded = Horde_Browser::wasFileUploaded($var->getVarName());
+            if (is_a($uploaded, 'PEAR_Error')) {
+                $message = $uploaded->getMessage();
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $name = $var->getVarName();
+        $uploaded = Horde_Browser::wasFileUploaded($name);
+        if ($uploaded === true) {
+            $info['name'] = Horde_Util::dispelMagicQuotes($_FILES[$name]['name']);
+            $info['type'] = $_FILES[$name]['type'];
+            $info['tmp_name'] = $_FILES[$name]['tmp_name'];
+            $info['file'] = $_FILES[$name]['tmp_name'];
+            $info['error'] = $_FILES[$name]['error'];
+            $info['size'] = $_FILES[$name]['size'];
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("File upload"));
+    }
+
+}
+
+class Horde_Form_Type_image extends Horde_Form_Type {
+
+    /**
+     * Has a file been uploaded on this form submit?
+     *
+     * @var boolean
+     */
+    var $_uploaded = null;
+
+    /**
+     * Show the upload button?
+     *
+     * @var boolean
+     */
+    var $_show_upload = true;
+
+    /**
+     * Show the option to upload also original non-modified image?
+     *
+     * @var boolean
+     */
+    var $_show_keeporig = false;
+
+    /**
+     * Limit the file size?
+     *
+     * @var integer
+     */
+    var $_max_filesize = null;
+
+    /**
+     * Hash containing the previously uploaded image info.
+     *
+     * @var array
+     */
+    var $_img;
+
+    /**
+     * A random id that identifies the image information in the session data.
+     *
+     * @var string
+     */
+    var $_random;
+
+    function init($show_upload = true, $show_keeporig = false, $max_filesize = null)
+    {
+        $this->_show_upload   = $show_upload;
+        $this->_show_keeporig = $show_keeporig;
+        $this->_max_filesize  = $max_filesize;
+    }
+
+    function onSubmit(&$var, &$vars)
+    {
+        /* Get the upload. */
+        $this->getImage($vars, $var);
+
+        /* If this was done through the upload button override the submitted
+         * value of the form. */
+        if ($vars->get('_do_' . $var->getVarName())) {
+            $var->form->setSubmitted(false);
+            if (is_a($this->_uploaded, 'PEAR_Error')) {
+                $this->_img = array('hash' => $this->getRandomId(),
+                                    'error' => $this->_uploaded->getMessage());
+            }
+        }
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        /* Get the upload. */
+        $this->getImage($vars, $var);
+        $field = $vars->get($var->getVarName());
+
+        /* The upload generated a PEAR Error. */
+        if (is_a($this->_uploaded, 'PEAR_Error')) {
+            /* Not required and no image upload attempted. */
+            if (!$var->isRequired() && empty($field['hash']) &&
+                $this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) {
+                return true;
+            }
+
+            if (($this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) &&
+                empty($field['hash'])) {
+                /* Nothing uploaded and no older upload. */
+                $message = _("This field is required.");
+                return false;
+            } elseif (!empty($field['hash'])) {
+                if ($this->_img && isset($this->_img['error'])) {
+                    $message = $this->_img['error'];
+                    return false;
+                }
+                /* Nothing uploaded but older upload present. */
+                return true;
+            } else {
+                /* Some other error message. */
+                $message = $this->_uploaded->getMessage();
+                return false;
+            }
+        } elseif (empty($this->_img['img']['size'])) {
+            $message = _("The image file size could not be determined or it was 0 bytes. The upload may have been interrupted.");
+            return false;
+        } elseif ($this->_max_filesize &&
+                  $this->_img['img']['size'] > $this->_max_filesize) {
+            $message = sprintf(_("The image file was larger than the maximum allowed size (%d bytes)."), $this->_max_filesize);
+            return false;
+        }
+
+        return true;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        /* Get the upload. */
+        $this->getImage($vars, $var);
+
+        /* Get image params stored in the hidden field. */
+        $value = $var->getValue($vars);
+        $info = $this->_img['img'];
+        if (empty($info['file'])) {
+            unset($info['file']);
+            return;
+        }
+        if ($this->_show_keeporig) {
+            $info['keep_orig'] = !empty($value['keep_orig']);
+        }
+
+        /* Set the uploaded value (either true or PEAR_Error). */
+        $info['uploaded'] = &$this->_uploaded;
+
+        /* If a modified file exists move it over the original. */
+        if ($this->_show_keeporig && $info['keep_orig']) {
+            /* Requested the saving of original file also. */
+            $info['orig_file'] = Horde::getTempDir() . '/' . $info['file'];
+            $info['file'] = Horde::getTempDir() . '/mod_' . $info['file'];
+            /* Check if a modified file actually exists. */
+            if (!file_exists($info['file'])) {
+                $info['file'] = $info['orig_file'];
+                unset($info['orig_file']);
+            }
+        } else {
+            /* Saving of original not required. */
+            $mod_file = Horde::getTempDir() . '/mod_' . $info['file'];
+            $info['file'] = Horde::getTempDir() . '/' . $info['file'];
+
+            if (file_exists($mod_file)) {
+                /* Unlink first (has to be done on Windows machines?) */
+                unlink($info['file']);
+                rename($mod_file, $info['file']);
+            }
+        }
+    }
+
+    /**
+     * Gets the upload and sets up the upload data array. Either
+     * fetches an upload done with this submit or retries stored
+     * upload info.
+     */
+    function _getUpload(&$vars, &$var)
+    {
+        /* Don't bother with this function if already called and set
+         * up vars. */
+        if (!empty($this->_img)) {
+            return true;
+        }
+
+        /* Check if file has been uploaded. */
+        $varname = $var->getVarName();
+        $this->_uploaded = Horde_Browser::wasFileUploaded($varname . '[new]');
+
+        if ($this->_uploaded === true) {
+            /* A file has been uploaded on this submit. Save to temp dir for
+             * preview work. */
+            $this->_img['img']['type'] = $this->getUploadedFileType($varname . '[new]');
+
+            /* Get the other parts of the upload. */
+            require_once 'Horde/Array.php';
+            Horde_Array::getArrayParts($varname . '[new]', $base, $keys);
+
+            /* Get the temporary file name. */
+            $keys_path = array_merge(array($base, 'tmp_name'), $keys);
+            $this->_img['img']['file'] = Horde_Array::getElement($_FILES, $keys_path);
+
+            /* Get the actual file name. */
+            $keys_path = array_merge(array($base, 'name'), $keys);
+            $this->_img['img']['name'] = Horde_Array::getElement($_FILES, $keys_path);
+
+            /* Get the file size. */
+            $keys_path = array_merge(array($base, 'size'), $keys);
+            $this->_img['img']['size'] = Horde_Array::getElement($_FILES, $keys_path);
+
+            /* Get any existing values for the image upload field. */
+            $upload = $vars->get($var->getVarName());
+            if (!empty($upload['hash'])) {
+                $upload['img'] = $_SESSION['horde_form'][$upload['hash']];
+                unset($_SESSION['horde_form'][$upload['hash']]);
+            }
+
+            /* Get the temp file if already one uploaded, otherwise create a
+             * new temporary file. */
+            if (!empty($upload['img']['file'])) {
+                $tmp_file = Horde::getTempDir() . '/' . $upload['img']['file'];
+            } else {
+                $tmp_file = Horde::getTempFile('Horde', false);
+            }
+
+            /* Move the browser created temp file to the new temp file. */
+            move_uploaded_file($this->_img['img']['file'], $tmp_file);
+            $this->_img['img']['file'] = basename($tmp_file);
+        } elseif ($this->_uploaded) {
+            /* File has not been uploaded. */
+            $upload = $vars->get($var->getVarName());
+            if ($this->_uploaded->getCode() == 4 &&
+                !empty($upload['hash']) &&
+                isset($_SESSION['horde_form'][$upload['hash']])) {
+                $this->_img['img'] = $_SESSION['horde_form'][$upload['hash']];
+                unset($_SESSION['horde_form'][$upload['hash']]);
+                if (isset($this->_img['error'])) {
+                    $this->_uploaded = PEAR::raiseError($this->_img['error']);
+                }
+            }
+        }
+        if (isset($this->_img['img'])) {
+            $_SESSION['horde_form'][$this->getRandomId()] = $this->_img['img'];
+        }
+    }
+
+    function getUploadedFileType($field)
+    {
+        /* Get any index on the field name. */
+        $index = Horde_Array::getArrayParts($field, $base, $keys);
+
+        if ($index) {
+            /* Index present, fetch the mime type var to check. */
+            $keys_path = array_merge(array($base, 'type'), $keys);
+            $type = Horde_Array::getElement($_FILES, $keys_path);
+            $keys_path = array_merge(array($base, 'tmp_name'), $keys);
+            $tmp_name = Horde_Array::getElement($_FILES, $keys_path);
+        } else {
+            /* No index, simple set up of vars to check. */
+            $type = $_FILES[$field]['type'];
+            $tmp_name = $_FILES[$field]['tmp_name'];
+        }
+
+        if (empty($type) || ($type == 'application/octet-stream')) {
+            /* Type wasn't set on upload, try analising the upload. */
+            if (!($type = Horde_Mime_Magic::analyzeFile($tmp_name, isset($GLOBALS['conf']['mime']['magic_db']) ? $GLOBALS['conf']['mime']['magic_db'] : null))) {
+                if ($index) {
+                    /* Get the name value. */
+                    $keys_path = array_merge(array($base, 'name'), $keys);
+                    $name = Horde_Array::getElement($_FILES, $keys_path);
+
+                    /* Work out the type from the file name. */
+                    $type = Horde_Mime_Magic::filenameToMime($name);
+
+                    /* Set the type. */
+                    $keys_path = array_merge(array($base, 'type'), $keys);
+                    Horde_Array::getElement($_FILES, $keys_path, $type);
+                } else {
+                    /* Work out the type from the file name. */
+                    $type = Horde_Mime_Magic::filenameToMime($_FILES[$field]['name']);
+
+                    /* Set the type. */
+                    $_FILES[$field]['type'] = Horde_Mime_Magic::filenameToMime($_FILES[$field]['name']);
+                }
+            }
+        }
+
+        return $type;
+    }
+
+    /**
+     * Returns the current image information.
+     *
+     * @return array  The current image hash.
+     */
+    function getImage($vars, $var)
+    {
+        $this->_getUpload($vars, $var);
+        if (!isset($this->_img)) {
+            $image = $vars->get($var->getVarName());
+            if ($image) {
+                $this->loadImageData($image);
+                if (isset($image['img'])) {
+                    $this->_img = $image;
+                    $_SESSION['horde_form'][$this->getRandomId()] = $this->_img['img'];
+                }
+            }
+        }
+        return $this->_img;
+    }
+
+    /**
+     * Loads any existing image data into the image field. Requires that the
+     * array $image passed to it contains the structure:
+     *   $image['load']['file'] - the filename of the image;
+     *   $image['load']['data'] - the raw image data.
+     *
+     * @param array $image  The image array.
+     */
+    function loadImageData(&$image)
+    {
+        /* No existing image data to load. */
+        if (!isset($image['load'])) {
+            return;
+        }
+
+        /* Save the data to the temp dir. */
+        $tmp_file = Horde::getTempDir() . '/' . $image['load']['file'];
+        if ($fd = fopen($tmp_file, 'w')) {
+            fwrite($fd, $image['load']['data']);
+            fclose($fd);
+        }
+
+        $image['img'] = array('file' => $image['load']['file']);
+        unset($image['load']);
+    }
+
+    function getRandomId()
+    {
+        if (!isset($this->_random)) {
+            $this->_random = uniqid(mt_rand());
+        }
+        return $this->_random;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Image upload"),
+            'params' => array(
+                'show_upload'   => array('label' => _("Show upload?"),
+                                         'type'  => 'boolean'),
+                'show_keeporig' => array('label' => _("Show option to keep original?"),
+                                         'type'  => 'boolean'),
+                'max_filesize'  => array('label' => _("Maximum file size in bytes"),
+                                         'type'  => 'int')));
+    }
+
+}
+
+class Horde_Form_Type_boolean extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = Horde_String::lower($vars->get($var->getVarName())) == 'on';
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("True or false"));
+    }
+
+}
+
+class Horde_Form_Type_link extends Horde_Form_Type {
+
+    /**
+     * List of hashes containing link parameters. Possible keys: 'url', 'text',
+     * 'target', 'onclick', 'title', 'accesskey'.
+     *
+     * @var array
+     */
+    var $values;
+
+    function init($values)
+    {
+        $this->values = $values;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Link"),
+            'params' => array(
+                'url' => array(
+                    'label' => _("Link URL"),
+                    'type' => 'text'),
+                'text' => array(
+                    'label' => _("Link text"),
+                    'type' => 'text'),
+                'target' => array(
+                    'label' => _("Link target"),
+                    'type' => 'text'),
+                'onclick' => array(
+                    'label' => _("Onclick event"),
+                    'type' => 'text'),
+                'title' => array(
+                    'label' => _("Link title attribute"),
+                    'type' => 'text'),
+                'accesskey' => array(
+                    'label' => _("Link access key"),
+                    'type' => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_email extends Horde_Form_Type {
+
+    /**
+     * Allow multiple addresses?
+     *
+     * @var boolean
+     */
+    var $_allow_multi = false;
+
+    /**
+     * Protect address from spammers?
+     *
+     * @var boolean
+     */
+    var $_strip_domain = false;
+
+    /**
+     * Link the email address to the compose page when displaying?
+     *
+     * @var boolean
+     */
+    var $_link_compose = false;
+
+    /**
+     * Whether to check the domain's SMTP server whether the address exists.
+     *
+     * @var boolean
+     */
+    var $_check_smtp = false;
+
+    /**
+     * The name to use when linking to the compose page
+     *
+     * @var boolean
+     */
+    var $_link_name;
+
+    /**
+     * A string containing valid delimiters (default is just comma).
+     *
+     * @var string
+     */
+    var $_delimiters = ',';
+
+    /**
+     * @param boolean $allow_multi   Allow multiple addresses?
+     * @param boolean $strip_domain  Protect address from spammers?
+     * @param boolean $link_compose  Link the email address to the compose page
+     *                               when displaying?
+     * @param string $link_name      The name to use when linking to the
+                                     compose page.
+     * @param string $delimiters     Character to split multiple addresses with.
+     */
+    function init($allow_multi = false, $strip_domain = false,
+                  $link_compose = false, $link_name = null,
+                  $delimiters = ',')
+    {
+        $this->_allow_multi = $allow_multi;
+        $this->_strip_domain = $strip_domain;
+        $this->_link_compose = $link_compose;
+        $this->_link_name = $link_name;
+        $this->_delimiters = $delimiters;
+    }
+
+    /**
+     */
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        // Split into individual addresses.
+        $emails = $this->splitEmailAddresses($value);
+
+        // Check for too many.
+        if (!$this->_allow_multi && count($emails) > 1) {
+            $message = _("Only one email address is allowed.");
+            return false;
+        }
+
+        // Check for all valid and at least one non-empty.
+        $nonEmpty = 0;
+        foreach ($emails as $email) {
+            if (!strlen($email)) {
+                continue;
+            }
+            if (!$this->validateEmailAddress($email)) {
+                $message = sprintf(_("\"%s\" is not a valid email address."), $email);
+                return false;
+            }
+            ++$nonEmpty;
+        }
+
+        if (!$nonEmpty && $var->isRequired()) {
+            if ($this->_allow_multi) {
+                $message = _("You must enter at least one email address.");
+            } else {
+                $message = _("You must enter an email address.");
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Explodes an RFC 2822 string, ignoring a delimiter if preceded
+     * by a "\" character, or if the delimiter is inside single or
+     * double quotes.
+     *
+     * @param string $string     The RFC 822 string.
+     *
+     * @return array  The exploded string in an array.
+     */
+    function splitEmailAddresses($string)
+    {
+        $quotes = array('"', "'");
+        $emails = array();
+        $pos = 0;
+        $in_quote = null;
+        $in_group = false;
+        $prev = null;
+
+        if (!strlen($string)) {
+            return array();
+        }
+
+        $char = $string[0];
+        if (in_array($char, $quotes)) {
+            $in_quote = $char;
+        } elseif ($char == ':') {
+            $in_group = true;
+        } elseif (strpos($this->_delimiters, $char) !== false) {
+            $emails[] = '';
+            $pos = 1;
+        }
+
+        for ($i = 1, $iMax = strlen($string); $i < $iMax; ++$i) {
+            $char = $string[$i];
+            if (in_array($char, $quotes)) {
+                if ($prev !== '\\') {
+                    if ($in_quote === $char) {
+                        $in_quote = null;
+                    } elseif (is_null($in_quote)) {
+                        $in_quote = $char;
+                    }
+                }
+            } elseif ($in_group) {
+                if ($char == ';') {
+                    $emails[] = substr($string, $pos, $i - $pos + 1);
+                    $pos = $i + 1;
+                    $in_group = false;
+                }
+            } elseif ($char == ':') {
+                $in_group = true;
+            } elseif (strpos($this->_delimiters, $char) !== false &&
+                      $prev !== '\\' &&
+                      is_null($in_quote)) {
+                $emails[] = substr($string, $pos, $i - $pos);
+                $pos = $i + 1;
+            }
+            $prev = $char;
+        }
+
+        if ($pos != $i) {
+            /* The string ended without a delimiter. */
+            $emails[] = substr($string, $pos, $i - $pos);
+        }
+
+        return $emails;
+    }
+
+    /**
+     * RFC(2)822 Email Parser.
+     *
+     * By Cal Henderson <cal@iamcal.com>
+     * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
+     * http://creativecommons.org/licenses/by-sa/2.5/
+     *
+     * http://code.iamcal.com/php/rfc822/
+     *
+     * http://iamcal.com/publish/articles/php/parsing_email
+     *
+     * Revision 4
+     *
+     * @param string $email An individual email address to validate.
+     *
+     * @return boolean
+     */
+    function validateEmailAddress($email)
+    {
+        static $comment_regexp, $email_regexp;
+        if ($comment_regexp === null) {
+            $this->_defineValidationRegexps($comment_regexp, $email_regexp);
+        }
+
+        // We need to strip comments first (repeat until we can't find
+        // any more).
+        while (true) {
+            $new = preg_replace("!$comment_regexp!", '', $email);
+            if (strlen($new) == strlen($email)){
+                break;
+            }
+            $email = $new;
+        }
+
+        // Now match what's left.
+        $result = (bool)preg_match("!^$email_regexp$!", $email);
+        if ($result && $this->_check_smtp) {
+            $result = $this->validateEmailAddressSmtp($email);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Attempt partial delivery of mail to an address to validate it.
+     *
+     * @param string $email An individual email address to validate.
+     *
+     * @return boolean
+     */
+    function validateEmailAddressSmtp($email)
+    {
+        list(, $maildomain) = explode('@', $email, 2);
+
+        // Try to get the real mailserver from MX records.
+        if (function_exists('getmxrr') &&
+            @getmxrr($maildomain, $mxhosts, $mxpriorities)) {
+            // MX record found.
+            array_multisort($mxpriorities, $mxhosts);
+            $mailhost = $mxhosts[0];
+        } else {
+            // No MX record found, try the root domain as the mail
+            // server.
+            $mailhost = $maildomain;
+        }
+
+        $fp = @fsockopen($mailhost, 25, $errno, $errstr, 5);
+        if (!$fp) {
+            return false;
+        }
+
+        // Read initial response.
+        fgets($fp, 4096);
+
+        // HELO
+        fputs($fp, "HELO $mailhost\r\n");
+        fgets($fp, 4096);
+
+        // MAIL FROM
+        fputs($fp, "MAIL FROM: <root@example.com>\r\n");
+        fgets($fp, 4096);
+
+        // RCPT TO - gets the result we want.
+        fputs($fp, "RCPT TO: <$email>\r\n");
+        $result = trim(fgets($fp, 4096));
+
+        // QUIT
+        fputs($fp, "QUIT\r\n");
+        fgets($fp, 4096);
+        fclose($fp);
+
+        return substr($result, 0, 1) == '2';
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Email"),
+            'params' => array(
+                'allow_multi' => array(
+                    'label' => _("Allow multiple addresses?"),
+                    'type'  => 'boolean'),
+                'strip_domain' => array(
+                    'label' => _("Protect address from spammers?"),
+                    'type' => 'boolean'),
+                'link_compose' => array(
+                    'label' => _("Link the email address to the compose page when displaying?"),
+                    'type' => 'boolean'),
+                'link_name' => array(
+                    'label' => _("The name to use when linking to the compose page"),
+                    'type' => 'text'),
+                'delimiters' => array(
+                    'label' => _("Character to split multiple addresses with"),
+                    'type' => 'text'),
+            ),
+        );
+    }
+
+    /**
+     * RFC(2)822 Email Parser.
+     *
+     * By Cal Henderson <cal@iamcal.com>
+     * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
+     * http://creativecommons.org/licenses/by-sa/2.5/
+     *
+     * http://code.iamcal.com/php/rfc822/
+     *
+     * http://iamcal.com/publish/articles/php/parsing_email
+     *
+     * Revision 4
+     *
+     * @param string &$comment The regexp for comments.
+     * @param string &$addr_spec The regexp for email addresses.
+     */
+    function _defineValidationRegexps(&$comment, &$addr_spec)
+    {
+        /**
+         * NO-WS-CTL       =       %d1-8 /         ; US-ASCII control characters
+         *                         %d11 /          ;  that do not include the
+         *                         %d12 /          ;  carriage return, line feed,
+         *                         %d14-31 /       ;  and white space characters
+         *                         %d127
+         * ALPHA          =  %x41-5A / %x61-7A   ; A-Z / a-z
+         * DIGIT          =  %x30-39
+         */
+        $no_ws_ctl  = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]";
+        $alpha      = "[\\x41-\\x5a\\x61-\\x7a]";
+        $digit      = "[\\x30-\\x39]";
+        $cr         = "\\x0d";
+        $lf         = "\\x0a";
+        $crlf       = "($cr$lf)";
+
+        /**
+         * obs-char        =       %d0-9 / %d11 /          ; %d0-127 except CR and
+         *                         %d12 / %d14-127         ;  LF
+         * obs-text        =       *LF *CR *(obs-char *LF *CR)
+         * text            =       %d1-9 /         ; Characters excluding CR and LF
+         *                         %d11 /
+         *                         %d12 /
+         *                         %d14-127 /
+         *                         obs-text
+         * obs-qp          =       "\" (%d0-127)
+         * quoted-pair     =       ("\" text) / obs-qp
+         */
+        $obs_char       = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]";
+        $obs_text       = "($lf*$cr*($obs_char$lf*$cr*)*)";
+        $text           = "([\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|$obs_text)";
+        $obs_qp         = "(\\x5c[\\x00-\\x7f])";
+        $quoted_pair    = "(\\x5c$text|$obs_qp)";
+
+        /**
+         * obs-FWS         =       1*WSP *(CRLF 1*WSP)
+         * FWS             =       ([*WSP CRLF] 1*WSP) /   ; Folding white space
+         *                         obs-FWS
+         * ctext           =       NO-WS-CTL /     ; Non white space controls
+         *                         %d33-39 /       ; The rest of the US-ASCII
+         *                         %d42-91 /       ;  characters not including "(",
+         *                         %d93-126        ;  ")", or "\"
+         * ccontent        =       ctext / quoted-pair / comment
+         * comment         =       "(" *([FWS] ccontent) [FWS] ")"
+         * CFWS            =       *([FWS] comment) (([FWS] comment) / FWS)
+         *
+         * @note: We translate ccontent only partially to avoid an
+         * infinite loop. Instead, we'll recursively strip comments
+         * before processing the input.
+         */
+        $wsp        = "[\\x20\\x09]";
+        $obs_fws    = "($wsp+($crlf$wsp+)*)";
+        $fws        = "((($wsp*$crlf)?$wsp+)|$obs_fws)";
+        $ctext      = "($no_ws_ctl|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])";
+        $ccontent   = "($ctext|$quoted_pair)";
+        $comment    = "(\\x28($fws?$ccontent)*$fws?\\x29)";
+        $cfws       = "(($fws?$comment)*($fws?$comment|$fws))";
+        $cfws       = "$fws*";
+
+        /**
+         * atext           =       ALPHA / DIGIT / ; Any character except controls,
+         *                         "!" / "#" /     ;  SP, and specials.
+         *                         "$" / "%" /     ;  Used for atoms
+         *                         "&" / "'" /
+         *                         "*" / "+" /
+         *                         "-" / "/" /
+         *                         "=" / "?" /
+         *                         "^" / "_" /
+         *                         "`" / "{" /
+         *                         "|" / "}" /
+         *                         "~"
+         * atom            =       [CFWS] 1*atext [CFWS]
+         */
+        $atext      = "($alpha|$digit|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2e\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])";
+        $atom       = "($cfws?$atext+$cfws?)";
+
+        /**
+         * qtext           =       NO-WS-CTL /     ; Non white space controls
+         *                         %d33 /          ; The rest of the US-ASCII
+         *                         %d35-91 /       ;  characters not including "\"
+         *                         %d93-126        ;  or the quote character
+         * qcontent        =       qtext / quoted-pair
+         * quoted-string   =       [CFWS]
+         *                         DQUOTE *([FWS] qcontent) [FWS] DQUOTE
+         *                         [CFWS]
+         * word            =       atom / quoted-string
+         */
+        $qtext      = "($no_ws_ctl|[\\x21\\x23-\\x5b\\x5d-\\x7e])";
+        $qcontent   = "($qtext|$quoted_pair)";
+        $quoted_string  = "($cfws?\\x22($fws?$qcontent)*$fws?\\x22$cfws?)";
+        $word       = "($atom|$quoted_string)";
+
+        /**
+         * obs-local-part  =       word *("." word)
+         * obs-domain      =       atom *("." atom)
+         */
+        $obs_local_part = "($word(\\x2e$word)*)";
+        $obs_domain = "($atom(\\x2e$atom)*)";
+
+        /**
+         * dot-atom-text   =       1*atext *("." 1*atext)
+         * dot-atom        =       [CFWS] dot-atom-text [CFWS]
+         */
+        $dot_atom_text  = "($atext+(\\x2e$atext+)*)";
+        $dot_atom   = "($cfws?$dot_atom_text$cfws?)";
+
+        /**
+         * domain-literal  =       [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
+         * dcontent        =       dtext / quoted-pair
+         * dtext           =       NO-WS-CTL /     ; Non white space controls
+         *
+         *                         %d33-90 /       ; The rest of the US-ASCII
+         *                         %d94-126        ;  characters not including "[",
+         *                                         ;  "]", or "\"
+         */
+        $dtext      = "($no_ws_ctl|[\\x21-\\x5a\\x5e-\\x7e])";
+        $dcontent   = "($dtext|$quoted_pair)";
+        $domain_literal = "($cfws?\\x5b($fws?$dcontent)*$fws?\\x5d$cfws?)";
+
+        /**
+         * local-part      =       dot-atom / quoted-string / obs-local-part
+         * domain          =       dot-atom / domain-literal / obs-domain
+         * addr-spec       =       local-part "@" domain
+         */
+        $local_part = "($dot_atom|$quoted_string|$obs_local_part)";
+        $domain     = "($dot_atom|$domain_literal|$obs_domain)";
+        $addr_spec  = "($local_part\\x40$domain)";
+    }
+
+}
+
+class Horde_Form_Type_matrix extends Horde_Form_Type {
+
+    var $_cols;
+    var $_rows;
+    var $_matrix;
+    var $_new_input;
+
+    /**
+     * Initializes the variable.
+     *
+     * Example:
+     * <code>
+     * init(array('Column A', 'Column B'),
+     *      array(1 => 'Row One', 2 => 'Row 2', 3 => 'Row 3'),
+     *      array(array(true, true, false),
+     *            array(true, false, true),
+     *            array(fasle, true, false)),
+     *      array('Row 4', 'Row 5'));
+     * </code>
+     *
+     * @param array $cols               A list of column headers.
+     * @param array $rows               A hash with row IDs as the keys and row
+     *                                  labels as the values.
+     * @param array $matrix             A two dimensional hash with the field
+     *                                  values.
+     * @param boolean|array $new_input  If true, a free text field to add a new
+     *                                  row is displayed on the top, a select
+     *                                  box if this parameter is a value.
+     */
+    function init($cols, $rows = array(), $matrix = array(), $new_input = false)
+    {
+        $this->_cols       = $cols;
+        $this->_rows       = $rows;
+        $this->_matrix     = $matrix;
+        $this->_new_input  = $new_input;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    function getCols()     { return $this->_cols; }
+    function getRows()     { return $this->_rows; }
+    function getMatrix()   { return $this->_matrix; }
+    function getNewInput() { return $this->_new_input; }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $values = $vars->get($var->getVarName());
+        if (!empty($values['n']['r']) && isset($values['n']['v'])) {
+            $new_row = $values['n']['r'];
+            $values['r'][$new_row] = $values['n']['v'];
+            unset($values['n']);
+        }
+
+        $info = (isset($values['r']) ? $values['r'] : array());
+    }
+
+    function about()
+    {
+        return array(
+            'name' => _("Field matrix"),
+            'params' => array(
+                'cols' => array('label' => _("Column titles"),
+                                'type'  => 'stringarray')));
+    }
+
+}
+
+class Horde_Form_Type_emailConfirm extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value['original'])) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if ($value['original'] != $value['confirm']) {
+            $message = _("Email addresses must match.");
+            return false;
+        } else {
+            $parsed_email = Horde_Mime_Address::parseAddressList($value['original'],
+                                                                 array('validate' => true));
+            if (is_a($parsed_email, 'PEAR_Error')) {
+                $message = $parsed_email->getMessage();
+                return false;
+            }
+            if (count($parsed_email) > 1) {
+                $message = _("Only one email address allowed.");
+                return false;
+            }
+            if (empty($parsed_email[0]->mailbox)) {
+                $message = _("You did not enter a valid email address.");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Email with confirmation"));
+    }
+
+}
+
+class Horde_Form_Type_password extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $valid = true;
+
+        if ($var->isRequired()) {
+            $valid = strlen(trim($value)) > 0;
+
+            if (!$valid) {
+                $message = _("This field is required.");
+            }
+        }
+
+        return $valid;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Password"));
+    }
+
+}
+
+class Horde_Form_Type_passwordconfirm extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value['original'])) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if ($value['original'] != $value['confirm']) {
+            $message = _("Passwords must match.");
+            return false;
+        }
+
+        return true;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $value = $vars->get($var->getVarName());
+        $info = $value['original'];
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Password with confirmation"));
+    }
+
+}
+
+class Horde_Form_Type_enum extends Horde_Form_Type {
+
+    var $_values;
+    var $_prompt;
+
+    function init($values, $prompt = null)
+    {
+        $this->setValues($values);
+
+        if ($prompt === true) {
+            $this->_prompt = _("-- select --");
+        } else {
+            $this->_prompt = $prompt;
+        }
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && $value == '' && !isset($this->_values[$value])) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (count($this->_values) == 0 || isset($this->_values[$value]) ||
+            ($this->_prompt && empty($value))) {
+            return true;
+        }
+
+        $message = _("Invalid data submitted.");
+        return false;
+    }
+
+    function getValues()
+    {
+        return $this->_values;
+    }
+
+    /**
+     * @since Horde 3.2
+     */
+    function setValues($values)
+    {
+        $this->_values = $values;
+    }
+
+    function getPrompt()
+    {
+        return $this->_prompt;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Drop down list"),
+            'params' => array(
+                'values' => array('label' => _("Values to select from"),
+                                  'type'  => 'stringarray'),
+                'prompt' => array('label' => _("Prompt text"),
+                                  'type'  => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_mlenum extends Horde_Form_Type {
+
+    var $_values;
+    var $_prompts;
+
+    function init(&$values, $prompts = null)
+    {
+        $this->_values = &$values;
+
+        if ($prompts === true) {
+            $this->_prompts = array(_("-- select --"), _("-- select --"));
+        } elseif (!is_array($prompts)) {
+            $this->_prompts = array($prompts, $prompts);
+        } else {
+            $this->_prompts = $prompts;
+        }
+    }
+
+    function onSubmit(&$var, &$vars)
+    {
+        $varname = $var->getVarName();
+        $value = $vars->get($varname);
+
+        if ($value['1'] != $value['old']) {
+            $var->form->setSubmitted(false);
+        }
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && (empty($value['1']) || empty($value['2']))) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (!count($this->_values) || isset($this->_values[$value['1']]) ||
+            (!empty($this->_prompts) && empty($value['1']))) {
+            return true;
+        }
+
+        $message = _("Invalid data submitted.");
+        return false;
+    }
+
+    function getValues()
+    {
+        return $this->_values;
+    }
+
+    function getPrompts()
+    {
+        return $this->_prompts;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = $vars->get($var->getVarName());
+        return $info['2'];
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Multi-level drop down lists"),
+            'params' => array(
+                'values' => array('label' => _("Values to select from"),
+                                  'type'  => 'stringarray'),
+                'prompt' => array('label' => _("Prompt text"),
+                                  'type'  => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_multienum extends Horde_Form_Type_enum {
+
+    var $size = 5;
+
+    function init($values, $size = null)
+    {
+        if (!is_null($size)) {
+            $this->size = (int)$size;
+        }
+
+        parent::init($values);
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (is_array($value)) {
+            foreach ($value as $val) {
+                if (!$this->isValid($var, $vars, $val, $message)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        if (empty($value) && ((string)(int)$value !== $value)) {
+            if ($var->isRequired()) {
+                $message = _("This field is required.");
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        if (count($this->_values) == 0 || isset($this->_values[$value])) {
+            return true;
+        }
+
+        $message = _("Invalid data submitted.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Multiple selection"),
+            'params' => array(
+                'values' => array('label' => _("Values"),
+                                  'type'  => 'stringarray'),
+                'size'   => array('label' => _("Size"),
+                                  'type'  => 'int'))
+        );
+    }
+
+}
+
+class Horde_Form_Type_keyval_multienum extends Horde_Form_Type_multienum {
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $value = $vars->get($var->getVarName());
+        $info = array();
+        foreach ($value as $key) {
+            $info[$key] = $this->_values[$key];
+        }
+    }
+
+}
+
+class Horde_Form_Type_radio extends Horde_Form_Type_enum {
+
+    /* Entirely implemented by Horde_Form_Type_enum; just a different
+     * view. */
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Radio selection"),
+            'params' => array(
+                'values' => array('label' => _("Values"),
+                                  'type'  => 'stringarray')));
+    }
+
+}
+
+class Horde_Form_Type_set extends Horde_Form_Type {
+
+    var $_values;
+    var $_checkAll = false;
+
+    function init($values, $checkAll = false)
+    {
+        $this->_values = $values;
+        $this->_checkAll = $checkAll;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (count($this->_values) == 0 || count($value) == 0) {
+            return true;
+        }
+        foreach ($value as $item) {
+            if (!isset($this->_values[$item])) {
+                $error = true;
+                break;
+            }
+        }
+        if (!isset($error)) {
+            return true;
+        }
+
+        $message = _("Invalid data submitted.");
+        return false;
+    }
+
+    function getValues()
+    {
+        return $this->_values;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Set"),
+            'params' => array(
+                'values' => array('label' => _("Values"),
+                                  'type'  => 'stringarray')));
+    }
+
+}
+
+class Horde_Form_Type_date extends Horde_Form_Type {
+
+    var $_format;
+
+    function init($format = '%a %d %B')
+    {
+        $this->_format = $format;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $valid = true;
+
+        if ($var->isRequired()) {
+            $valid = strlen(trim($value)) > 0;
+
+            if (!$valid) {
+                $message = sprintf(_("%s is required"), $var->getHumanName());
+            }
+        }
+
+        return $valid;
+    }
+
+    /**
+     * @static
+     *
+     * @param mixed $date  The date to calculate the difference from. Can be
+     *                     either a timestamp integer value, or an array
+     *                     with date parts: 'day', 'month', 'year'.
+     *
+     * @return string
+     */
+    function getAgo($date)
+    {
+        if ($date === null) {
+            return '';
+        } elseif (!is_array($date)) {
+            /* Date is not array, so assume timestamp. Work out the component
+             * parts using date(). */
+            $date = array('day'   => date('j', $date),
+                          'month' => date('n', $date),
+                          'year'  => date('Y', $date));
+        }
+
+        require_once 'Date/Calc.php';
+        $diffdays = Date_Calc::dateDiff((int)$date['day'],
+                                        (int)$date['month'],
+                                        (int)$date['year'],
+                                        date('j'), date('n'), date('Y'));
+
+        /* An error occured. */
+        if ($diffdays == -1) {
+            return;
+        }
+
+        $ago = $diffdays * Date_Calc::compareDates((int)$date['day'],
+                                                   (int)$date['month'],
+                                                   (int)$date['year'],
+                                                   date('j'), date('n'),
+                                                   date('Y'));
+        if ($ago < -1) {
+            return sprintf(_(" (%s days ago)"), $diffdays);
+        } elseif ($ago == -1) {
+            return _(" (yesterday)");
+        } elseif ($ago == 0) {
+            return _(" (today)");
+        } elseif ($ago == 1) {
+            return _(" (tomorrow)");
+        } else {
+            return sprintf(_(" (in %s days)"), $diffdays);
+        }
+    }
+
+    function getFormattedTime($timestamp, $format = null, $showago = true)
+    {
+        if (empty($format)) {
+            $format = $this->_format;
+        }
+        if (!empty($timestamp)) {
+            return strftime($format, $timestamp) . ($showago ? Horde_Form_Type_date::getAgo($timestamp) : '');
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Date"));
+    }
+
+}
+
+class Horde_Form_Type_time extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || preg_match('/^[0-2]?[0-9]:[0-5][0-9]$/', $value)) {
+            return true;
+        }
+
+        $message = _("This field may only contain numbers and the colon.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Time"));
+    }
+
+}
+
+class Horde_Form_Type_hourminutesecond extends Horde_Form_Type {
+
+    var $_show_seconds;
+
+    function init($show_seconds = false)
+    {
+        $this->_show_seconds = $show_seconds;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $time = $vars->get($var->getVarName());
+        if (!$this->_show_seconds && count($time) && !isset($time['second'])) {
+            $time['second'] = 0;
+        }
+
+        if (!$this->emptyTimeArray($time) && !$this->checktime($time['hour'], $time['minute'], $time['second'])) {
+            $message = _("Please enter a valid time.");
+            return false;
+        } elseif ($this->emptyTimeArray($time) && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        return true;
+    }
+
+    function checktime($hour, $minute, $second)
+    {
+        if (!isset($hour) || $hour == '' || ($hour < 0 || $hour > 23)) {
+            return false;
+        }
+        if (!isset($minute) || $minute == '' || ($minute < 0 || $minute > 60)) {
+            return false;
+        }
+        if (!isset($second) || $second === '' || ($second < 0 || $second > 60)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the time supplied as a Horde_Date object.
+     *
+     * @param string $time_in  Date in one of the three formats supported by
+     *                         Horde_Form and Horde_Date (ISO format
+     *                         YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and
+     *                         UNIX epoch).
+     *
+     * @return Date  The time object.
+     */
+    function getTimeOb($time_in)
+    {
+        require_once 'Horde/Date.php';
+
+        if (is_array($time_in)) {
+            if (!$this->emptyTimeArray($time_in)) {
+                $time_in = sprintf('1970-01-01 %02d:%02d:%02d', $time_in['hour'], $time_in['minute'], $this->_show_seconds ? $time_in['second'] : 0);
+            }
+        }
+
+        return new Horde_Date($time_in);
+    }
+
+    /**
+     * Return the time supplied split up into an array.
+     *
+     * @param string $time_in  Time in one of the three formats supported by
+     *                         Horde_Form and Horde_Date (ISO format
+     *                         YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and
+     *                         UNIX epoch).
+     *
+     * @return array  Array with three elements - hour, minute and seconds.
+     */
+    function getTimeParts($time_in)
+    {
+        if (is_array($time_in)) {
+            /* This is probably a failed isValid input so just return the
+             * parts as they are. */
+            return $time_in;
+        } elseif (empty($time_in)) {
+            /* This is just an empty field so return empty parts. */
+            return array('hour' => '', 'minute' => '', 'second' => '');
+        }
+        $time = $this->getTimeOb($time_in);
+        return array('hour' => $time->hour,
+                     'minute' => $time->min,
+                     'second' => $time->sec);
+    }
+
+    function emptyTimeArray($time)
+    {
+        return (is_array($time)
+                && (!isset($time['hour']) || !strlen($time['hour']))
+                && (!isset($time['minute']) || !strlen($time['minute']))
+                && (!$this->_show_seconds || !strlen($time['second'])));
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Time selection"),
+            'params' => array(
+                'seconds' => array('label' => _("Show seconds?"),
+                                   'type'  => 'boolean')));
+    }
+
+}
+
+class Horde_Form_Type_monthyear extends Horde_Form_Type {
+
+    var $_start_year;
+    var $_end_year;
+
+    function init($start_year = null, $end_year = null)
+    {
+        if (empty($start_year)) {
+            $start_year = 1920;
+        }
+        if (empty($end_year)) {
+            $end_year = date('Y');
+        }
+
+        $this->_start_year = $start_year;
+        $this->_end_year = $end_year;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (!$var->isRequired()) {
+            return true;
+        }
+
+        if (!$vars->get($this->getMonthVar($var)) ||
+            !$vars->get($this->getYearVar($var))) {
+            $message = _("Please enter a month and a year.");
+            return false;
+        }
+
+        return true;
+    }
+
+    function getMonthVar($var)
+    {
+        return $var->getVarName() . '[month]';
+    }
+
+    function getYearVar($var)
+    {
+        return $var->getVarName() . '[year]';
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Month and year"),
+                     'params' => array(
+                         'start_year' => array('label' => _("Start year"),
+                                               'type'  => 'int'),
+                         'end_year'   => array('label' => _("End year"),
+                                               'type'  => 'int')));
+    }
+
+}
+
+class Horde_Form_Type_monthdayyear extends Horde_Form_Type {
+
+    var $_start_year;
+    var $_end_year;
+    var $_picker;
+    var $_format_in = null;
+    var $_format_out = '%x';
+
+    /**
+     * Return the date supplied as a Horde_Date object.
+     *
+     * @param integer $start_year  The first available year for input.
+     * @param integer $end_year    The last available year for input.
+     * @param boolean $picker      Do we show the DHTML calendar?
+     * @param integer $format_in   The format to use when sending the date
+     *                             for storage. Defaults to Unix epoch.
+     *                             Similar to the strftime() function.
+     * @param integer $format_out  The format to use when displaying the
+     *                             date. Similar to the strftime() function.
+     */
+    function init($start_year = '', $end_year = '', $picker = true,
+                  $format_in = null, $format_out = '%x')
+    {
+        if (empty($start_year)) {
+            $start_year = date('Y');
+        }
+        if (empty($end_year)) {
+            $end_year = date('Y') + 10;
+        }
+
+        $this->_start_year = $start_year;
+        $this->_end_year = $end_year;
+        $this->_picker = $picker;
+        $this->_format_in = $format_in;
+        $this->_format_out = $format_out;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $date = $vars->get($var->getVarName());
+        $empty = $this->emptyDateArray($date);
+
+        if ($empty == 1 && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        } elseif ($empty == 0 && !checkdate($date['month'],
+                                            $date['day'],
+                                            $date['year'])) {
+            $message = _("Please enter a valid date, check the number of days in the month.");
+            return false;
+        } elseif ($empty == -1) {
+            $message = _("Select all date components.");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Determine if the provided date value is completely empty, partially empty
+     * or non-empty.
+     *
+     * @param mixed $date  String or date part array representation of date.
+     *
+     * @return integer  0 for non-empty, 1 for completely empty or -1 for
+     *                  partially empty.
+     */
+    function emptyDateArray($date)
+    {
+        if (!is_array($date)) {
+            return (int)empty($date);
+        }
+        $empty = 0;
+        /* Check each date array component. */
+        foreach (array('day', 'month', 'year') as $key) {
+            if (empty($date[$key])) {
+                $empty++;
+            }
+        }
+
+        /* Check state of empty. */
+        if ($empty == 0) {
+            /* If no empty parts return 0. */
+            return 0;
+        } elseif ($empty == 3) {
+            /* If all empty parts return 1. */
+            return 1;
+        } else {
+            /* If some empty parts return -1. */
+            return -1;
+        }
+    }
+
+    /**
+     * Return the date supplied split up into an array.
+     *
+     * @param string $date_in  Date in one of the three formats supported by
+     *                         Horde_Form and Horde_Date (ISO format
+     *                         YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS
+     *                         and UNIX epoch) plus the fourth YYYY-MM-DD.
+     *
+     * @return array  Array with three elements - year, month and day.
+     */
+    function getDateParts($date_in)
+    {
+        if (is_array($date_in)) {
+            /* This is probably a failed isValid input so just return
+             * the parts as they are. */
+            return $date_in;
+        } elseif (empty($date_in)) {
+            /* This is just an empty field so return empty parts. */
+            return array('year' => '', 'month' => '', 'day' => '');
+        }
+
+        $date = $this->getDateOb($date_in);
+        return array('year' => $date->year,
+                     'month' => $date->month,
+                     'day' => $date->mday);
+    }
+
+    /**
+     * Return the date supplied as a Horde_Date object.
+     *
+     * @param string $date_in  Date in one of the three formats supported by
+     *                         Horde_Form and Horde_Date (ISO format
+     *                         YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS
+     *                         and UNIX epoch) plus the fourth YYYY-MM-DD.
+     *
+     * @return Date  The date object.
+     */
+    function getDateOb($date_in)
+    {
+        require_once 'Horde/Date.php';
+
+        if (is_array($date_in)) {
+            /* If passed an array change it to the ISO format. */
+            if ($this->emptyDateArray($date_in) == 0) {
+                $date_in = sprintf('%04d-%02d-%02d 00:00:00',
+                                   $date_in['year'],
+                                   $date_in['month'],
+                                   $date_in['day']);
+            }
+        } elseif (preg_match('/^\d{4}-?\d{2}-?\d{2}$/', $date_in)) {
+            /* Fix the date if it is the shortened ISO. */
+            $date_in = $date_in . ' 00:00:00';
+        }
+
+        return new Horde_Date($date_in);
+    }
+
+    /**
+     * Return the date supplied as a Horde_Date object.
+     *
+     * @param string $date  Either an already set up Horde_Date object or a
+     *                      string date in one of the three formats supported
+     *                      by Horde_Form and Horde_Date (ISO format
+     *                      YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and
+     *                      UNIX epoch) plus the fourth YYYY-MM-DD.
+     *
+     * @return string  The date formatted according to the $format_out
+     *                 parameter when setting up the monthdayyear field.
+     */
+    function formatDate($date)
+    {
+        if (!is_a($date, 'Date')) {
+            $date = $this->getDateOb($date);
+        }
+
+        return $date->strftime($this->_format_out);
+    }
+
+    /**
+     * Insert the date input through the form into $info array, in the format
+     * specified by the $format_in parameter when setting up monthdayyear
+     * field.
+     */
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = $this->_validateAndFormat($var->getValue($vars), $var);
+    }
+
+    /**
+     * Validate/format a date submission.
+     */
+    function _validateAndFormat($value, &$var)
+    {
+        /* If any component is empty consider it a bad date and return the
+         * default. */
+        if ($this->emptyDateArray($value) == 1) {
+            return $var->getDefault();
+        } else {
+            $date = $this->getDateOb($value);
+            if ($this->_format_in === null) {
+                return $date->timestamp();
+            } else {
+                return $date->strftime($this->_format_in);
+            }
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Date selection"),
+            'params' => array(
+                'start_year' => array('label' => _("Start year"),
+                                      'type'  => 'int'),
+                'end_year'   => array('label' => _("End year"),
+                                      'type'  => 'int'),
+                'picker'     => array('label' => _("Show picker?"),
+                                      'type'  => 'boolean'),
+                'format_in'  => array('label' => _("Storage format"),
+                                      'type'  => 'text'),
+                'format_out' => array('label' => _("Display format"),
+                                      'type'  => 'text')));
+    }
+
+}
+
+/**
+ * @since Horde 3.2
+ */
+class Horde_Form_Type_datetime extends Horde_Form_Type {
+
+    var $_mdy;
+    var $_hms;
+    var $_show_seconds;
+
+    /**
+     * Return the date supplied as a Horde_Date object.
+     *
+     * @param integer $start_year  The first available year for input.
+     * @param integer $end_year    The last available year for input.
+     * @param boolean $picker      Do we show the DHTML calendar?
+     * @param integer $format_in   The format to use when sending the date
+     *                             for storage. Defaults to Unix epoch.
+     *                             Similar to the strftime() function.
+     * @param integer $format_out  The format to use when displaying the
+     *                             date. Similar to the strftime() function.
+     * @param boolean $show_seconds Include a form input for seconds.
+     */
+    function init($start_year = '', $end_year = '', $picker = true,
+                  $format_in = null, $format_out = '%x', $show_seconds = false)
+    {
+        $this->_mdy = new Horde_Form_Type_monthdayyear();
+        $this->_mdy->init($start_year, $end_year, $picker, $format_in, $format_out);
+
+        $this->_hms = new Horde_Form_Type_hourminutesecond();
+        $this->_hms->init($show_seconds);
+        $this->_show_seconds = $show_seconds;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        $date = $vars->get($var->getVarName());
+        if (!$this->_show_seconds && !isset($date['second'])) {
+            $date['second'] = '';
+        }
+        $mdy_empty = $this->emptyDateArray($date);
+        $hms_empty = $this->emptyTimeArray($date);
+
+        $valid = true;
+
+        /* Require all fields if one field is not empty */
+        if ($var->isRequired() || $mdy_empty != 1 || !$hms_empty) {
+            $old_required = $var->required;
+            $var->required = true;
+
+            $mdy_valid = $this->_mdy->isValid($var, $vars, $value, $message);
+            $hms_valid = $this->_hms->isValid($var, $vars, $value, $message);
+            $var->required = $old_required;
+
+            $valid = $mdy_valid && $hms_valid;
+            if ($mdy_valid && !$hms_valid) {
+                $message = _("You must choose a time.");
+            } elseif ($hms_valid && !$mdy_valid) {
+                $message = _("You must choose a date.");
+            }
+        }
+
+        return $valid;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        /* If any component is empty consider it a bad date and return the
+         * default. */
+        $value = $var->getValue($vars);
+        if ($this->emptyDateArray($value) == 1 || $this->emptyTimeArray($value)) {
+            $info = $var->getDefault();
+            return;
+        }
+
+        $date = $this->getDateOb($value);
+        $time = $this->getTimeOb($value);
+        $date->hour = $time->hour;
+        $date->min = $time->min;
+        $date->sec = $time->sec;
+        if ($this->getProperty('format_in') === null) {
+            $info = $date->timestamp();
+        } else {
+            $info = $date->strftime($this->getProperty('format_in'));
+        }
+    }
+
+    function getProperty($property)
+    {
+        if ($property == 'show_seconds') {
+            return $this->_hms->getProperty($property);
+        } else {
+            return $this->_mdy->getProperty($property);
+        }
+    }
+
+    function setProperty($property, $value)
+    {
+        if ($property == 'show_seconds') {
+            $this->_hms->setProperty($property, $value);
+        } else {
+            $this->_mdy->setProperty($property, $value);
+        }
+    }
+
+    function checktime($hour, $minute, $second)
+    {
+        return $this->_hms->checktime($hour, $minute, $second);
+    }
+
+    function getTimeOb($time_in)
+    {
+        return $this->_hms->getTimeOb($time_in);
+    }
+
+    function getTimeParts($time_in)
+    {
+        return $this->_hms->getTimeParts($time_in);
+    }
+
+    function emptyTimeArray($time)
+    {
+        return $this->_hms->emptyTimeArray($time);
+    }
+
+    function emptyDateArray($date)
+    {
+        return $this->_mdy->emptyDateArray($date);
+    }
+
+    function getDateParts($date_in)
+    {
+        return $this->_mdy->getDateParts($date_in);
+    }
+
+    function getDateOb($date_in)
+    {
+        return $this->_mdy->getDateOb($date_in);
+    }
+
+    function formatDate($date)
+    {
+        if ($this->_mdy->emptyDateArray($date)) {
+            return '';
+        }
+        return $this->_mdy->formatDate($date);
+    }
+
+    function about()
+    {
+        return array(
+            'name' => _("Date and time selection"),
+            'params' => array(
+                'start_year' => array('label' => _("Start year"),
+                                      'type'  => 'int'),
+                'end_year'   => array('label' => _("End year"),
+                                      'type'  => 'int'),
+                'picker'     => array('label' => _("Show picker?"),
+                                      'type'  => 'boolean'),
+                'format_in'  => array('label' => _("Storage format"),
+                                      'type'  => 'text'),
+                'format_out' => array('label' => _("Display format"),
+                                      'type'  => 'text'),
+                'seconds'    => array('label' => _("Show seconds?"),
+                                      'type'  => 'boolean')));
+    }
+
+}
+
+class Horde_Form_Type_colorpicker extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value)) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || preg_match('/^#([0-9a-z]){6}$/i', $value)) {
+            return true;
+        }
+
+        $message = _("This field must contain a color code in the RGB Hex format, for example '#1234af'.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Colour selection"));
+    }
+
+}
+
+class Horde_Form_Type_sound extends Horde_Form_Type {
+
+    var $_sounds = array();
+
+    function init()
+    {
+        foreach (glob($GLOBALS['registry']->get('themesfs', 'horde') . '/sounds/*.wav') as $sound) {
+            $this->_sounds[] = basename($sound);
+        }
+    }
+
+    function getSounds()
+    {
+        return $this->_sounds;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if ($var->isRequired() && empty($value)) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (empty($value) || in_array($value, $this->_sounds)) {
+            return true;
+        }
+
+        $message = _("Please choose a sound.");
+        return false;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Sound selection"));
+    }
+
+}
+
+class Horde_Form_Type_sorter extends Horde_Form_Type {
+
+    var $_instance;
+    var $_values;
+    var $_size;
+    var $_header;
+
+    function init($values, $size = 8, $header = '')
+    {
+        static $horde_sorter_instance = 0;
+
+        /* Get the next progressive instance count for the horde
+         * sorter so that multiple sorters can be used on one page. */
+        $horde_sorter_instance++;
+        $this->_instance = 'horde_sorter_' . $horde_sorter_instance;
+        $this->_values = $values;
+        $this->_size   = $size;
+        $this->_header = $header;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    function getValues()
+    {
+        return $this->_values;
+    }
+
+    function getSize()
+    {
+        return $this->_size;
+    }
+
+    function getHeader()
+    {
+        if (!empty($this->_header)) {
+            return $this->_header;
+        }
+        return '';
+    }
+
+    function getOptions($keys = null)
+    {
+        $html = '';
+        if ($this->_header) {
+            $html .= '<option value="">' . htmlspecialchars($this->_header) . '</option>';
+        }
+
+        if (empty($keys)) {
+            $keys = array_keys($this->_values);
+        } else {
+            $keys = explode("\t", $keys['array']);
+        }
+        foreach ($keys as $sl_key) {
+            $html .= '<option value="' . $sl_key . '">' . htmlspecialchars($this->_values[$sl_key]) . '</option>';
+        }
+
+        return $html;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $value = $vars->get($var->getVarName());
+        $info = explode("\t", $value['array']);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Sort order selection"),
+            'params' => array(
+                'values' => array('label' => _("Values"),
+                                  'type'  => 'stringarray'),
+                'size'   => array('label' => _("Size"),
+                                  'type'  => 'int'),
+                'header' => array('label' => _("Header"),
+                                  'type'  => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_selectfiles extends Horde_Form_Type {
+
+    /**
+     * The text to use in the link.
+     *
+     * @var string
+     */
+    var $_link_text;
+
+    /**
+     * The style to use for the link.
+     *
+     * @var string
+     */
+    var $_link_style;
+
+    /**
+     *  Create the link with an icon instead of text?
+     *
+     * @var boolean
+     */
+    var $_icon;
+
+    /**
+     * Contains gollem selectfile selectionID
+     *
+     * @var string
+     */
+    var $_selectid;
+
+    function init($selectid, $link_text = null, $link_style = '',
+                  $icon = false)
+    {
+        $this->_selectid = $selectid;
+        if (is_null($link_text)) {
+            $link_text = _("Select Files");
+        }
+        $this->_link_text = $link_text;
+        $this->_link_style = $link_style;
+        $this->_icon = $icon;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    function getInfo(&$var, &$vars, &$info)
+    {
+        $value = $vars->getValue($var);
+        $info = $GLOBALS['registry']->call('files/selectlistResults', array($value));
+    }
+
+    function about()
+    {
+        return array(
+            'name' => _("File selection"),
+            'params' => array(
+                'selectid'   => array('label' => _("Id"),
+                                      'type' => 'text'),
+                'link_text'  => array('label' => _("Link text"),
+                                      'type' => 'text'),
+                'link_style' => array('label' => _("Link style"),
+                                      'type' => 'text'),
+                'icon'       => array('label' => _("Show icon?"),
+                                      'type' => 'boolean')));
+    }
+
+}
+
+class Horde_Form_Type_assign extends Horde_Form_Type {
+
+    var $_leftValues;
+    var $_rightValues;
+    var $_leftHeader;
+    var $_rightHeader;
+    var $_size;
+    var $_width;
+
+    function init($leftValues, $rightValues, $leftHeader = '',
+                  $rightHeader = '', $size = 8, $width = '200px')
+    {
+        $this->_leftValues = $leftValues;
+        $this->_rightValues = $rightValues;
+        $this->_leftHeader = $leftHeader;
+        $this->_rightHeader = $rightHeader;
+        $this->_size = $size;
+        $this->_width = $width;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    function getValues($side)
+    {
+        return $side ? $this->_rightValues : $this->_leftValues;
+    }
+
+    function setValues($side, $values)
+    {
+        if ($side) {
+            $this->_rightValues = $values;
+        } else {
+            $this->_leftValues = $values;
+        }
+    }
+
+    function getHeader($side)
+    {
+        return $side ? $this->_rightHeader : $this->_leftHeader;
+    }
+
+    function getSize()
+    {
+        return $this->_size;
+    }
+
+    function getWidth()
+    {
+        return $this->_width;
+    }
+
+    function getOptions($side, $formname, $varname)
+    {
+        $html = '';
+        $headers = false;
+        if ($side) {
+            $values = $this->_rightValues;
+            if (!empty($this->_rightHeader)) {
+                $values = array('' => $this->_rightHeader) + $values;
+                $headers = true;
+            }
+        } else {
+            $values = $this->_leftValues;
+            if (!empty($this->_leftHeader)) {
+                $values = array('' => $this->_leftHeader) + $values;
+                $headers = true;
+            }
+        }
+
+        foreach ($values as $key => $val) {
+            $html .= '<option value="' . htmlspecialchars($key) . '"';
+            if ($headers) {
+                $headers = false;
+            } else {
+                $html .= ' ondblclick="Horde_Form_Assign.move(\'' . $formname . '\', \'' . $varname . '\', ' . (int)$side . ');"';
+            }
+            $html .= '>' . htmlspecialchars($val) . '</option>';
+        }
+
+        return $html;
+    }
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $value = $vars->get($var->getVarName() . '__values');
+        if (strpos($value, "\t\t") === false) {
+            $left = $value;
+            $right = '';
+        } else {
+            list($left, $right) = explode("\t\t", $value);
+        }
+        if (empty($left)) {
+            $info['left'] = array();
+        } else {
+            $info['left'] = explode("\t", $left);
+        }
+        if (empty($right)) {
+            $info['right'] = array();
+        } else {
+            $info['right'] = explode("\t", $right);
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Assignment columns"),
+            'params' => array(
+                'leftValues'  => array('label' => _("Left values"),
+                                       'type'  => 'stringarray'),
+                'rightValues' => array('label' => _("Right values"),
+                                       'type'  => 'stringarray'),
+                'leftHeader'  => array('label' => _("Left header"),
+                                       'type'  => 'text'),
+                'rightHeader' => array('label' => _("Right header"),
+                                       'type'  => 'text'),
+                'size'        => array('label' => _("Size"),
+                                       'type'  => 'int'),
+                'width'       => array('label' => _("Width in CSS units"),
+                                       'type'  => 'text')));
+    }
+
+}
+
+class Horde_Form_Type_creditcard extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (empty($value) && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (!empty($value)) {
+            /* getCardType() will also verify the checksum. */
+            $type = $this->getCardType($value);
+            if ($type === false || $type == 'unknown') {
+                $message = _("This does not seem to be a valid card number.");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    function getChecksum($ccnum)
+    {
+        $len = strlen($ccnum);
+        if (!is_long($len / 2)) {
+            $weight = 2;
+            $digit = $ccnum[0];
+        } elseif (is_long($len / 2)) {
+            $weight = 1;
+            $digit = $ccnum[0] * 2;
+        }
+        if ($digit > 9) {
+            $digit = $digit - 9;
+        }
+        $i = 1;
+        $checksum = $digit;
+        while ($i < $len) {
+            if ($ccnum[$i] != ' ') {
+                $digit = $ccnum[$i] * $weight;
+                $weight = ($weight == 1) ? 2 : 1;
+                if ($digit > 9) {
+                    $digit = $digit - 9;
+                }
+                $checksum += $digit;
+            }
+            $i++;
+        }
+
+        return $checksum;
+    }
+
+    function getCardType($ccnum)
+    {
+        $sum = $this->getChecksum($ccnum);
+        $l = strlen($ccnum);
+
+        // Screen checksum.
+        if (($sum % 10) != 0) {
+            return false;
+        }
+
+        // Check for Visa.
+        if ((($l == 16) || ($l == 13)) &&
+            ($ccnum[0] == 4)) {
+            return 'visa';
+        }
+
+        // Check for MasterCard.
+        if (($l == 16) &&
+            ($ccnum[0] == 5) &&
+            ($ccnum[1] >= 1) &&
+            ($ccnum[1] <= 5)) {
+            return 'mastercard';
+        }
+
+        // Check for Amex.
+        if (($l == 15) &&
+            ($ccnum[0] == 3) &&
+            (($ccnum[1] == 4) || ($ccnum[1] == 7))) {
+            return 'amex';
+        }
+
+        // Check for Discover (Novus).
+        if (strlen($ccnum) == 16 &&
+            substr($ccnum, 0, 4) == '6011') {
+            return 'discover';
+        }
+
+        // If we got this far, then no card matched.
+        return 'unknown';
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Credit card number"));
+    }
+
+}
+
+class Horde_Form_Type_obrowser extends Horde_Form_Type {
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return true;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Relationship browser"));
+    }
+
+}
+
+class Horde_Form_Type_dblookup extends Horde_Form_Type_enum {
+
+    function init($dsn, $sql, $prompt = null)
+    {
+        require_once 'DB.php';
+        $values = array();
+        $db = DB::connect($dsn);
+        if (!is_a($db, 'PEAR_Error')) {
+            // Set DB portability options.
+            switch ($db->phptype) {
+            case 'mssql':
+                $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+
+            $col = $db->getCol($sql);
+            if (!is_a($col, 'PEAR_Error')) {
+                $values = array_combine($col, $col);
+            }
+        }
+        parent::init($values, $prompt);
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Database lookup"),
+            'params' => array(
+                'dsn' => array('label' => _("DSN (see http://pear.php.net/manual/en/package.database.db.intro-dsn.php)"),
+                               'type'  => 'text'),
+                'sql' => array('label' => _("SQL statement for value lookups"),
+                               'type'  => 'text'),
+                'prompt' => array('label' => _("Prompt text"),
+                                  'type'  => 'text'))
+            );
+    }
+
+}
+
+class Horde_Form_Type_figlet extends Horde_Form_Type {
+
+    var $_text;
+    var $_font;
+
+    function init($text, $font)
+    {
+        $this->_text = $text;
+        $this->_font = $font;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (empty($value) && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        if (Horde_String::lower($value) != Horde_String::lower($this->_text)) {
+            $message = _("The text you entered did not match the text on the screen.");
+            return false;
+        }
+
+        return true;
+    }
+
+    function getFont()
+    {
+        return $this->_font;
+    }
+
+    function getText()
+    {
+        return $this->_text;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Figlet CAPTCHA"),
+            'params' => array(
+                'text' => array('label' => _("Text"),
+                                'type'  => 'text'),
+                'font' => array('label' => _("Figlet font"),
+                                'type'  => 'text'))
+            );
+    }
+
+}
+
+class Horde_Form_Type_captcha extends Horde_Form_Type_figlet {
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Image CAPTCHA"),
+            'params' => array(
+                'text' => array('label' => _("Text"),
+                                'type'  => 'text'),
+                'font' => array('label' => _("Font"),
+                                'type'  => 'text'))
+            );
+    }
+
+}
+
+/**
+ * @since Horde 3.2
+ */
+class Horde_Form_Type_category extends Horde_Form_Type {
+
+    function getInfo(&$vars, &$var, &$info)
+    {
+        $info = $var->getValue($vars);
+        if ($info == '*new*') {
+            $info = array('new' => true,
+                          'value' => $vars->get('new_category'));
+        } else {
+            $info = array('new' => false,
+                          'value' => $info);
+        }
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array('name' => _("Category"));
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (empty($value) && $var->isRequired()) {
+            $message = _("This field is required.");
+            return false;
+        }
+
+        return true;
+    }
+
+}
+
+class Horde_Form_Type_invalid extends Horde_Form_Type {
+
+    var $message;
+
+    function init($message)
+    {
+        $this->message = $message;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        return false;
+    }
+
+}
diff --git a/framework/Form/Form/Type/tableset.php b/framework/Form/Form/Type/tableset.php
new file mode 100644 (file)
index 0000000..77f5d49
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+require_once 'Horde/Form.php';
+
+/**
+ * $Horde: framework/Form/Form/Type/tableset.php,v 1.3 2005/05/31 08:42:01 jan Exp $
+ *
+ * @package Horde_Form
+ * @since   Horde 3.1
+ */
+class Horde_Form_Type_tableset extends Horde_Form_Type {
+
+    var $_values;
+    var $_header;
+
+    function init($values, $header)
+    {
+        $this->_values = $values;
+        $this->_header = $header;
+    }
+
+    function isValid(&$var, &$vars, $value, &$message)
+    {
+        if (count($this->_values) == 0 || count($value) == 0) {
+            return true;
+        }
+        foreach ($value as $item) {
+            if (!isset($this->_values[$item])) {
+                $error = true;
+                break;
+            }
+        }
+        if (!isset($error)) {
+            return true;
+        }
+
+        $message = _("Invalid data submitted.");
+        return false;
+    }
+
+    function getHeader()
+    {
+        return $this->_header;
+    }
+
+    function getValues()
+    {
+        return $this->_values;
+    }
+
+    /**
+     * Return info about field type.
+     */
+    function about()
+    {
+        return array(
+            'name' => _("Table Set"),
+            'params' => array(
+                'values' => array('label' => _("Values"),
+                                  'type'  => 'stringlist'),
+                'header' => array('label' => _("Headers"),
+                                  'type'  => 'stringlist')),
+            );
+    }
+
+}
diff --git a/framework/Form/Form/Variable.php b/framework/Form/Form/Variable.php
new file mode 100644 (file)
index 0000000..c7efba3
--- /dev/null
@@ -0,0 +1,520 @@
+<?php
+/**
+ * This class represents a single form variable that may be rendered as one or
+ * more form fields.
+ *
+ * @author  Robert E. Coyle <robertecoyle@hotmail.com>
+ * @package Horde_Form
+ */
+class Horde_Form_Variable {
+
+    /**
+     * The form instance this variable is assigned to.
+     *
+     * @var Horde_Form
+     */
+    var $form;
+
+    /**
+     * A short description of this variable's purpose.
+     *
+     * @var string
+     */
+    var $humanName;
+
+    /**
+     * The internally used name.
+     *
+     * @var string
+     */
+    var $varName;
+
+    /**
+     * A {@link Horde_Form_Type} instance.
+     *
+     * @var Horde_Form_Type
+     */
+    var $type;
+
+    /**
+     * Whether this is a required variable.
+     *
+     * @var boolean
+     */
+    var $required;
+
+    /**
+     * Whether this is a readonly variable.
+     *
+     * @var boolean
+     */
+    var $readonly;
+
+    /**
+     * A long description of the variable's purpose, special instructions, etc.
+     *
+     * @var string
+     */
+    var $description;
+
+    /**
+     * The variable help text.
+     *
+     * @var string
+     */
+    var $help;
+
+    /**
+     * Whether this is an array variable.
+     *
+     * @var boolean
+     */
+    var $_arrayVal;
+
+    /**
+     * The default value.
+     *
+     * @var mixed
+     */
+    var $_defValue = null;
+
+    /**
+     * A {@link Horde_Form_Action} instance.
+     *
+     * @var Horde_Form_Action
+     */
+    var $_action;
+
+    /**
+     * Whether this variable is disabled.
+     *
+     * @var boolean
+     */
+    var $_disabled = false;
+
+    /**
+     * TODO
+     *
+     * @var boolean
+     */
+    var $_autofilled = false;
+
+    /**
+     * Whether this is a hidden variable.
+     *
+     * @var boolean
+     */
+    var $_hidden = false;
+
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    var $_options = array();
+
+    /**
+     * Variable constructor.
+     *
+     * @param string $humanName      A short description of the variable's
+     *                               purpose.
+     * @param string $varName        The internally used name.
+     * @param Horde_Form_Type $type  A {@link Horde_Form_Type} instance.
+     * @param boolean $required      Whether this is a required variable.
+     * @param boolean $readonly      Whether this is a readonly variable.
+     * @param string $description    A long description of the variable's
+     *                               purpose, special instructions, etc.
+     */
+    function Horde_Form_Variable($humanName, $varName, &$type, $required,
+                                 $readonly = false, $description = null)
+    {
+        $this->humanName   = $humanName;
+        $this->varName     = $varName;
+        $this->type        = &$type;
+        $this->required    = $required;
+        $this->readonly    = $readonly;
+        $this->description = $description;
+        $this->_arrayVal   = (strpos($varName, '[]') !== false);
+    }
+
+    /**
+     * Assign this variable to the specified form.
+     *
+     * @param Horde_Form $form  The form instance to assign this variable to.
+     */
+    function setFormOb(&$form)
+    {
+        $this->form = &$form;
+    }
+
+    /**
+     * Sets a default value for this variable.
+     *
+     * @param mixed $value  A variable value.
+     */
+    function setDefault($value)
+    {
+        $this->_defValue = $value;
+    }
+
+    /**
+     * Returns this variable's default value.
+     *
+     * @return mixed  This variable's default value.
+     */
+    function getDefault()
+    {
+        return $this->_defValue;
+    }
+
+    /**
+     * Assigns an action to this variable.
+     *
+     * Example:
+     * <code>
+     * $v = &$form->addVariable('My Variable', 'var1', 'text', false);
+     * $v->setAction(Horde_Form_Action::factory('submit'));
+     * </code>
+     *
+     * @param Horde_Form_Action $action  A {@link Horde_Form_Action} instance.
+     */
+    function setAction($action)
+    {
+        $this->_action = $action;
+    }
+
+    /**
+     * Returns whether this variable has an attached action.
+     *
+     * @return boolean  True if this variable has an attached action.
+     */
+    function hasAction()
+    {
+        return !is_null($this->_action);
+    }
+
+    /**
+     * Makes this a hidden variable.
+     */
+    function hide()
+    {
+        $this->_hidden = true;
+    }
+
+    /**
+     * Returns whether this is a hidden variable.
+     *
+     * @return boolean  True if this a hidden variable.
+     */
+    function isHidden()
+    {
+        return $this->_hidden;
+    }
+
+    /**
+     * Disables this variable.
+     */
+    function disable()
+    {
+        $this->_disabled = true;
+    }
+
+    /**
+     * Returns whether this variable is disabled.
+     *
+     * @return boolean  True if this variable is disabled.
+     */
+    function isDisabled()
+    {
+        return $this->_disabled;
+    }
+
+    /**
+     * Return the short description of this variable.
+     *
+     * @return string  A short description
+     */
+    function getHumanName()
+    {
+        return $this->humanName;
+    }
+
+    /**
+     * Returns the internally used variable name.
+     *
+     * @return string  This variable's internal name.
+     */
+    function getVarName()
+    {
+        return $this->varName;
+    }
+
+    /**
+     * Returns this variable's type.
+     *
+     * @return Horde_Form_Type  This variable's {@link Horde_Form_Type}
+     *                          instance.
+     */
+    function &getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Returns the name of this variable's type.
+     *
+     * @return string  This variable's {@link Horde_Form_Type} name.
+     */
+    function getTypeName()
+    {
+        return $this->type->getTypeName();
+    }
+
+    /**
+     * Returns whether this is a required variable.
+     *
+     * @return boolean  True if this is a required variable.
+     */
+    function isRequired()
+    {
+        return $this->required;
+    }
+
+    /**
+     * Returns whether this is a readonly variable.
+     *
+     * @return boolean  True if this a readonly variable.
+     */
+    function isReadonly()
+    {
+        return $this->readonly;
+    }
+
+    /**
+     * Returns the possible values of this variable.
+     *
+     * @return array  The possible values of this variable or null.
+     */
+    function getValues()
+    {
+        return $this->type->getValues();
+    }
+
+    /**
+     * Returns whether this variable has a long description.
+     *
+     * @return boolean  True if this variable has a long description.
+     */
+    function hasDescription()
+    {
+        return !empty($this->description);
+    }
+
+    /**
+     * Returns this variable's long description.
+     *
+     * @return string  This variable's long description.
+     */
+    function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Returns whether this is an array variable.
+     *
+     * @return boolean  True if this an array variable.
+     */
+    function isArrayVal()
+    {
+        return $this->_arrayVal;
+    }
+
+    /**
+     * Returns whether this variable is to upload a file.
+     *
+     * @return boolean  True if variable is to upload a file.
+     */
+    function isUpload()
+    {
+        return ($this->type->getTypeName() == 'file');
+    }
+
+    /**
+     * Assigns a help text to this variable.
+     *
+     * @param string $help  The variable help text.
+     */
+    function setHelp($help)
+    {
+        $this->form->_help = true;
+        $this->help = $help;
+    }
+
+    /**
+     * Returns whether this variable has some help text assigned.
+     *
+     * @return boolean  True if this variable has a help text.
+     */
+    function hasHelp()
+    {
+        return !empty($this->help);
+    }
+
+    /**
+     * Returns the help text of this variable.
+     *
+     * @return string  This variable's help text.
+     */
+    function getHelp()
+    {
+        return $this->help;
+    }
+
+    /**
+     * Sets a variable option.
+     *
+     * @param string $option  The option name.
+     * @param mixed $val      The option's value.
+     */
+    function setOption($option, $val)
+    {
+        $this->_options[$option] = $val;
+    }
+
+    /**
+     * Returns a variable option's value.
+     *
+     * @param string $option  The option name.
+     *
+     * @return mixed          The option's value.
+     */
+    function getOption($option)
+    {
+        return isset($this->_options[$option]) ? $this->_options[$option] : null;
+    }
+
+    /**
+     * Processes the submitted value of this variable according to the rules of
+     * the variable type.
+     *
+     * @param Variables $vars  The {@link Variables} instance of the submitted
+     *                         form.
+     * @param mixed $info      A variable passed by reference that will be
+     *                         assigned the processed value of the submitted
+     *                         variable value.
+     *
+     * @return mixed  Depending on the variable type.
+     */
+    function getInfo(&$vars, &$info)
+    {
+        return $this->type->getInfo($vars, $this, $info);
+    }
+
+    /**
+     * Returns whether this variable if it had the "trackchange" option set
+     * has actually been changed.
+     *
+     * @param Variables $vars  The {@link Variables} instance of the submitted
+     *                         form.
+     *
+     * @return boolean  Null if this variable doesn't have the "trackchange"
+     *                  option set or the form wasn't submitted yet. A boolean
+     *                  indicating whether the variable was changed otherwise.
+     */
+    function wasChanged(&$vars)
+    {
+        if (!$this->getOption('trackchange')) {
+            return null;
+        }
+        $old = $vars->get('__old_' . $this->getVarName());
+        if (is_null($old)) {
+            return null;
+        }
+        return $old != $vars->get($this->getVarName());
+    }
+
+    /**
+     * Validates this variable.
+     *
+     * @param Variables $vars  The {@link Variables} instance of the submitted
+     *                         form.
+     * @param string $message  A variable passed by reference that will be
+     *                         assigned a descriptive error message if
+     *                         validation failed.
+     *
+     * @return boolean  True if the variable validated.
+     */
+    function validate(&$vars, &$message)
+    {
+        if ($this->_arrayVal) {
+            $vals = $this->getValue($vars);
+            if (!is_array($vals)) {
+                if ($this->required) {
+                    $message = _("This field is required.");
+                    return false;
+                } else {
+                    return true;
+                }
+            }
+            foreach ($vals as $i => $value) {
+                if ($value === null && $this->required) {
+                    $message = _("This field is required.");
+                    return false;
+                } else {
+                    if (!$this->type->isValid($this, $vars, $value, $message)) {
+                        return false;
+                    }
+                }
+            }
+        } else {
+            $value = $this->getValue($vars);
+            return $this->type->isValid($this, $vars, $value, $message);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the submitted or default value of this variable.
+     * If an action is attached to this variable, the value will get passed to
+     * the action object.
+     *
+     * @param Variables $vars  The {@link Variables} instance of the submitted
+     *                         form.
+     * @param integer $index   If the variable is an array variable, this
+     *                         specifies the array element to return.
+     *
+     * @return mixed  The variable or element value.
+     */
+    function getValue(&$vars, $index = null)
+    {
+        if ($this->_arrayVal) {
+            $name = str_replace('[]', '', $this->varName);
+        } else {
+            $name = $this->varName;
+        }
+        $value = $vars->getExists($name, $wasset);
+
+        if (!$wasset) {
+            $value = $this->getDefault();
+        }
+
+        if ($this->_arrayVal && !is_null($index)) {
+            if (!$wasset && !is_array($value)) {
+                $return = $value;
+            } else {
+                $return = isset($value[$index]) ? $value[$index] : null;
+            }
+        } else {
+            $return = $value;
+        }
+
+        if ($this->hasAction()) {
+            $this->_action->setValues($vars, $return, $this->_arrayVal);
+        }
+
+        return $return;
+    }
+
+}
diff --git a/framework/Form/package.xml b/framework/Form/package.xml
new file mode 100644 (file)
index 0000000..791c616
--- /dev/null
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Form</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Form API</summary>
+ <description>The Horde_Form:: package provides form rendering, validation, and other functionality for the Horde Application Framework.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>21:43:34</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="Form">
+    <dir name="Action">
+     <file baseinstalldir="/Horde" name="ConditionalEnable.php" role="php" />
+     <file baseinstalldir="/Horde" name="ConditionalSetValue.php" role="php" />
+     <file baseinstalldir="/Horde" name="conditional_enable.php" role="php" />
+     <file baseinstalldir="/Horde" name="conditional_setvalue.php" role="php" />
+     <file baseinstalldir="/Horde" name="reload.php" role="php" />
+     <file baseinstalldir="/Horde" name="setcursorpos.php" role="php" />
+     <file baseinstalldir="/Horde" name="submit.php" role="php" />
+     <file baseinstalldir="/Horde" name="sum_fields.php" role="php" />
+     <file baseinstalldir="/Horde" name="updatefield.php" role="php" />
+    </dir> <!-- //Form/Action -->
+    <dir name="Type">
+     <file baseinstalldir="/Horde" name="tableset.php" role="php" />
+    </dir> <!-- //Form/Type -->
+    <file baseinstalldir="/Horde" name="Action.php" role="php" />
+    <file baseinstalldir="/Horde" name="Renderer.php" role="php" />
+    <file baseinstalldir="/Horde" name="Type.php" role="php" />
+    <file baseinstalldir="/Horde" name="Variable.php" role="php" />
+   </dir> <!-- //Form -->
+   <file baseinstalldir="/Horde" name="Form.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Token</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Form/tests/Horde_Form_Type_address.phpt b/framework/Form/tests/Horde_Form_Type_address.phpt
new file mode 100644 (file)
index 0000000..d3f08aa
--- /dev/null
@@ -0,0 +1,175 @@
+--TEST--
+Horde_Form_Type_address tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/../Form.php';
+
+$addresses = array(
+    // UK addresses.
+    '11 Foo Bar
+2nd Row
+London W3 8JN',
+    '999 Church Street
+London
+N9 9HT',
+
+    // German addresses.
+    'Hauptstr. 1
+D-11111 Stadt',
+    'Nebenweg 13
+22222 Hintertupfing
+Germany',
+
+    // Canadian addresses.
+    '1000 Sesame Street
+Vancouver, BC V6C 3P1',
+
+    // U.S. addresses.
+    '99 Foo Street
+Boston, MA 02141
+USA',
+    'First line
+101 Main Road
+Chelmsford, Massachusetts 01824',
+    // This one cannot be parsed correctly:
+    '3000 Woodstock Boulevard
+Portland, Oregon
+USA',
+
+    // Various countries.
+    'Foo-Bar-Str. 99
+12345 Hinterm Wald',
+    '33602 Bielefeld',
+    'Some first line
+A 2nd address line 51
+33333 Somewhere',
+    'Straat 123
+9717 Groningen
+Unknown Country'
+);
+
+foreach ($addresses as $address) {
+    var_dump(Horde_Form_Type_address::parse($address));
+}
+
+?>
+--EXPECT--
+array(4) {
+  ["country"]=>
+  string(2) "uk"
+  ["zip"]=>
+  string(6) "W3 8JN"
+  ["street"]=>
+  string(18) "11 Foo Bar
+2nd Row"
+  ["city"]=>
+  string(6) "London"
+}
+array(4) {
+  ["country"]=>
+  string(2) "uk"
+  ["zip"]=>
+  string(6) "N9 9HT"
+  ["street"]=>
+  string(17) "999 Church Street"
+  ["city"]=>
+  string(6) "London"
+}
+array(4) {
+  ["street"]=>
+  string(11) "Hauptstr. 1"
+  ["country"]=>
+  string(2) "de"
+  ["zip"]=>
+  string(5) "11111"
+  ["city"]=>
+  string(5) "Stadt"
+}
+array(4) {
+  ["street"]=>
+  string(11) "Nebenweg 13"
+  ["country"]=>
+  string(2) "de"
+  ["zip"]=>
+  string(5) "22222"
+  ["city"]=>
+  string(13) "Hintertupfing"
+}
+array(5) {
+  ["country"]=>
+  string(2) "ca"
+  ["street"]=>
+  string(18) "1000 Sesame Street"
+  ["city"]=>
+  string(9) "Vancouver"
+  ["state"]=>
+  string(2) "BC"
+  ["zip"]=>
+  string(7) "V6C 3P1"
+}
+array(5) {
+  ["country"]=>
+  string(2) "us"
+  ["street"]=>
+  string(13) "99 Foo Street"
+  ["city"]=>
+  string(6) "Boston"
+  ["state"]=>
+  string(2) "MA"
+  ["zip"]=>
+  string(5) "02141"
+}
+array(5) {
+  ["country"]=>
+  string(2) "us"
+  ["street"]=>
+  string(24) "First line
+101 Main Road"
+  ["city"]=>
+  string(10) "Chelmsford"
+  ["state"]=>
+  string(13) "Massachusetts"
+  ["zip"]=>
+  string(5) "01824"
+}
+array(3) {
+  ["street"]=>
+  string(16) "Portland, Oregon"
+  ["zip"]=>
+  string(4) "3000"
+  ["city"]=>
+  string(19) "Woodstock Boulevard"
+}
+array(3) {
+  ["street"]=>
+  string(15) "Foo-Bar-Str. 99"
+  ["zip"]=>
+  string(5) "12345"
+  ["city"]=>
+  string(12) "Hinterm Wald"
+}
+array(2) {
+  ["zip"]=>
+  string(5) "33602"
+  ["city"]=>
+  string(9) "Bielefeld"
+}
+array(3) {
+  ["street"]=>
+  string(37) "Some first line
+A 2nd address line 51"
+  ["zip"]=>
+  string(5) "33333"
+  ["city"]=>
+  string(9) "Somewhere"
+}
+array(3) {
+  ["street"]=>
+  string(26) "Straat 123
+Unknown Country"
+  ["zip"]=>
+  string(4) "9717"
+  ["city"]=>
+  string(9) "Groningen"
+}
diff --git a/framework/Form/tests/Horde_Form_Type_email.phpt b/framework/Form/tests/Horde_Form_Type_email.phpt
new file mode 100644 (file)
index 0000000..66f4dcc
--- /dev/null
@@ -0,0 +1,100 @@
+--TEST--
+Horde_Form_Type_email tests
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/../Form.php';
+
+$vars = Horde_Variables::getDefaultVariables();
+$type = new Horde_Form_Type_email();
+$var = new Horde_Form_Variable('email add', 'email', $type, true);
+
+function test($email)
+{
+    global $type, $var, $vars;
+    $valid = $type->isValid($var, $vars, $email, $message);
+    echo $valid ? 'Yes' : 'No: ' . $message;
+    echo "\n";
+}
+
+test('cal@iamcalx.com');
+test('cal+henderson@iamcalx.com');
+test('cal henderson@iamcalx.com');
+test('"cal henderson"@iamcalx.com');
+test('cal@iamcalx');
+test('cal@iamcalx com');
+test('cal@hello world.com');
+test('cal@[hello].com');
+test('cal@[hello world].com');
+test('cal@[hello\\ world].com');
+test('cal@[hello.com]');
+test('cal@[hello world.com]');
+test('cal@[hello\\ world.com]');
+test('abcdefghijklmnopqrstuvwxyz@abcdefghijklmnopqrstuvwxyz');
+
+test('woo\\ yay@example.com');
+test('woo\\@yay@example.com');
+test('woo\\.yay@example.com');
+
+test('"woo yay"@example.com');
+test('"woo@yay"@example.com');
+test('"woo.yay"@example.com');
+test('"woo\\"yay"@test.com');
+
+test('webstaff@redcross.org');
+
+test('');
+test(',');
+test(',,,,');
+test('chuck@horde.org,');
+test('chuck@horde.org,,');
+test('cal@iamcalx.com, foo@example.com');
+test(',chuck@horde.org,');
+
+$type->_allow_multi = true;
+test('');
+test(',');
+test(',,,,');
+test('chuck@horde.org,');
+test('chuck@horde.org,,');
+test('cal@iamcalx.com, foo@example.com');
+test(',chuck@horde.org,');
+
+?>
+--EXPECT--
+Yes
+Yes
+No: "cal henderson@iamcalx.com" is not a valid email address.
+Yes
+Yes
+No: "cal@iamcalx com" is not a valid email address.
+No: "cal@hello world.com" is not a valid email address.
+No: "cal@[hello].com" is not a valid email address.
+No: "cal@[hello world].com" is not a valid email address.
+No: "cal@[hello\ world].com" is not a valid email address.
+Yes
+Yes
+Yes
+Yes
+No: "woo\ yay@example.com" is not a valid email address.
+No: "woo\@yay@example.com" is not a valid email address.
+No: "woo\.yay@example.com" is not a valid email address.
+Yes
+Yes
+Yes
+Yes
+Yes
+No: You must enter an email address.
+No: You must enter an email address.
+No: Only one email address is allowed.
+Yes
+No: Only one email address is allowed.
+No: Only one email address is allowed.
+No: Only one email address is allowed.
+No: You must enter at least one email address.
+No: You must enter at least one email address.
+No: You must enter at least one email address.
+Yes
+Yes
+Yes
+Yes
diff --git a/framework/Group/Group.php b/framework/Group/Group.php
new file mode 100644 (file)
index 0000000..d95eadf
--- /dev/null
@@ -0,0 +1,910 @@
+<?php
+
+require_once 'Horde/DataTree.php';
+
+/** The parent Group node */
+define('GROUP_ROOT', -1);
+
+/**
+ * The Group:: class provides the Horde groups system.
+ *
+ * $Horde: framework/Group/Group.php,v 1.122 2009/10/04 03:03:45 chuck Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stephane Huther <shuther1@free.fr>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 2.1
+ * @package Horde_Group
+ */
+class Group {
+
+    /**
+     * Group driver parameters
+     *
+     * @var array
+     */
+    var $_params;
+
+    /**
+     * Pointer to a DataTree instance to manage the different groups.
+     *
+     * @var DataTree
+     */
+    var $_datatree;
+
+    /**
+     * Cache of previously retrieved group objects.
+     *
+     * @var array
+     */
+    var $_groupCache = array();
+
+    /**
+     * Id-name-map of already cached group objects.
+     *
+     * @var array
+     */
+    var $_groupMap = array();
+
+    /**
+     * Id-name-hash of all existing groups.
+     *
+     * @var array
+     */
+    var $_groupList;
+
+    /**
+     * List of sub groups.
+     *
+     * @see listAllUsers()
+     * @var array
+     */
+    var $_subGroups = array();
+
+    /**
+     * Cache of parent groups.
+     *
+     * This is an array with group IDs as keys and the integer group id of the
+     * direct parent as values.
+     *
+     * @see getGroupParent
+     * @var array
+     */
+    var $_groupParents = array();
+
+    /**
+     * Cache of parent group trees.
+     *
+     * This is an array with group IDs as keys and id-name-hashes of all
+     * parents as values.
+     *
+     * @see getGroupParentList
+     * @var array
+     */
+    var $_groupParentList = array();
+
+    /**
+     * Cache of parents tree.
+     *
+     * @see getGroupParents()
+     * @var array
+     */
+    var $_parentTree = array();
+
+    /**
+     * Hash of groups of certain users.
+     *
+     * @see getGroupMemberShips()
+     * @var array
+     */
+    var $_userGroups;
+
+    /**
+     * Constructor.
+     */
+    function Group($params)
+    {
+        $this->_params = $params;
+        $this->__wakeup();
+    }
+
+    /**
+     * Initializes the object.
+     *
+     * @throws Horde_Exception
+     */
+    function __wakeup()
+    {
+        global $conf;
+
+        if (empty($conf['datatree']['driver'])) {
+            throw new Horde_Exception('You must configure a DataTree backend to use Groups.');
+        }
+
+        $driver = $conf['datatree']['driver'];
+        $this->_datatree = &DataTree::singleton($driver,
+                                                array_merge(Horde::getDriverConfig('datatree', $driver),
+                                                            array('group' => 'horde.groups')));
+
+        foreach (array_keys($this->_groupCache) as $name) {
+            $this->_groupCache[$name]->setGroupOb($this);
+            $this->_groupCache[$name]->setDataTree($this->_datatree);
+        }
+    }
+
+    /**
+     * Returns a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's name.
+     *
+     * @return DataTreeObject_Group  A new group object.
+     */
+    function &newGroup($name, $parent = GROUP_ROOT)
+    {
+        if (empty($name)) {
+            return PEAR::raiseError(_("Group names must be non-empty"));
+        }
+
+        if ($parent != GROUP_ROOT) {
+            $name = $this->getGroupName($parent) . ':' . DataTree::encodeName($name);
+        }
+
+        $group = new DataTreeObject_Group($name);
+        $group->setGroupOb($this);
+        return $group;
+    }
+
+    /**
+     * Returns a DataTreeObject_Group object corresponding to the named group,
+     * with the users and other data retrieved appropriately.
+     *
+     * @param string $name The name of the group to retrieve.
+     */
+    function &getGroup($name)
+    {
+        if (!isset($this->_groupCache[$name])) {
+            $this->_groupCache[$name] = &$this->_datatree->getObject($name, 'DataTreeObject_Group');
+            if (!is_a($this->_groupCache[$name], 'PEAR_Error')) {
+                $this->_groupCache[$name]->setGroupOb($this);
+                $this->_groupMap[$this->_groupCache[$name]->getId()] = $name;
+            }
+        }
+
+        return $this->_groupCache[$name];
+    }
+
+    /**
+     * Returns a DataTreeObject_Group object corresponding to the given unique
+     * ID, with the users and other data retrieved appropriately.
+     *
+     * @param integer $cid  The unique ID of the group to retrieve.
+     */
+    function &getGroupById($cid)
+    {
+        if (isset($this->_groupMap[$cid])) {
+            $group = $this->_groupCache[$this->_groupMap[$cid]];
+        } else {
+            $group = $this->_datatree->getObjectById($cid, 'DataTreeObject_Group');
+            if (!is_a($group, 'PEAR_Error')) {
+                $group->setGroupOb($this);
+                $name = $group->getName();
+                $this->_groupCache[$name] = &$group;
+                $this->_groupMap[$cid] = $name;
+            }
+        }
+
+        return $group;
+    }
+
+    /**
+     * Returns a globally unique ID for a group.
+     *
+     * @param DataTreeObject_Group $group  The group.
+     *
+     * @return string  A GUID referring to $group.
+     */
+    function getGUID($group)
+    {
+        return 'horde:group:' . $this->getGroupId($group);
+    }
+
+    /**
+     * Adds a group to the groups system. The group must first be created with
+     * Group::newGroup(), and have any initial users added to it, before this
+     * function is called.
+     *
+     * @param DataTreeObject_Group $group  The new group object.
+     */
+    function addGroup($group)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+        $result = $this->_datatree->add($group);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $id = $group->getId();
+        $name = $group->getName();
+        $this->_groupCache[$name] = &$group;
+        $this->_groupMap[$id] = $name;
+        if (isset($this->_groupList)) {
+            $this->_groupList[$id] = $name;
+        }
+
+        /* Log the addition of the group in the history log. */
+        $history = &Horde_History::singleton();
+        $log = $history->log($this->getGUID($group), array('action' => 'add'), true);
+        if (is_a($log, 'PEAR_Error')) {
+            return $log;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Stores updated data - users, etc. - of a group to the backend system.
+     *
+     * @param DataTreeObject_Group $group  The group to update.
+     */
+    function updateGroup($group)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+        $result = $this->_datatree->updateData($group);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_groupCache[$group->getName()] = &$group;
+
+        /* Log the update of the group users on the history log. */
+        $history = &Horde_History::singleton();
+        $guid = $this->getGUID($group);
+        foreach ($group->getAuditLog() as $userId => $action) {
+            $history->log($guid, array('action' => $action, 'user' => $userId), true);
+        }
+        $group->clearAuditLog();
+
+        /* Log the group modification. */
+        $history->log($guid, array('action' => 'modify'), true);
+        return $result;
+    }
+
+    /**
+     * Removes a group from the groups system permanently.
+     *
+     * @param DataTreeObject_Group $group  The group to remove.
+     * @param boolean $force               Force to remove every child.
+     */
+    function removeGroup($group, $force = false)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+
+        $id = $group->getId();
+        unset($this->_groupMap[$id]);
+        if (isset($this->_groupList)) {
+            unset($this->_groupList[$id]);
+        }
+        unset($this->_groupCache[$group->getName()]);
+
+        $history = &Horde_History::singleton();
+        $history->log($this->getGUID($group), array('action' => 'delete'), true);
+
+        return $this->_datatree->remove($group, $force);
+    }
+
+    /**
+     * Retrieves the name of a group.
+     *
+     * @param integer|DataTreeObject_Group $gid  The id of the group or the
+     *                                           group object to retrieve the
+     *                                           name for.
+     *
+     * @return string  The group's name.
+     */
+    function getGroupName($gid)
+    {
+        if (is_a($gid, 'DataTreeObject_Group')) {
+            $gid = $gid->getId();
+        }
+
+        if (isset($this->_groupMap[$gid])) {
+            return $this->_groupMap[$gid];
+        }
+        if (isset($this->_groupList[$gid])) {
+            return $this->_groupList[$gid];
+        }
+
+        return $this->_datatree->getName($gid);
+    }
+
+    /**
+     * Strips all parent references off of the given group name.
+     *
+     * @param string $group  Name of the group.
+     *
+     * @return The name of the group without parents.
+     */
+    function getGroupShortName($group)
+    {
+        return $this->_datatree->getShortName($group);
+    }
+
+    /**
+     * Retrieves the ID of a group.
+     *
+     * @param string|DataTreeObject_Group $group  The group name or object to
+     *                                            retrieve the ID for.
+     *
+     * @return integer  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        if (is_a($group, 'DataTreeObject_Group')) {
+            $group = $group->getName();
+        }
+
+        $id = array_search($group, $this->_groupMap);
+        if ($id !== false) {
+            return $id;
+        }
+        if (isset($this->_groupList)) {
+            $id = array_search($group, $this->_groupList);
+            if ($id !== false) {
+                return $id;
+            }
+        }
+
+        return $this->_datatree->getId($group);
+    }
+
+    /**
+     * Check if a group exists in the system.
+     *
+     * @param string $group  The group to check.
+     *
+     * @return boolean  True if the group exists, false otherwise.
+     */
+    function exists($group)
+    {
+        if (isset($this->_groupCache[$group]) ||
+            (isset($this->_groupList) &&
+             array_search($group, $this->_groupList) !== false)) {
+            return true;
+        }
+
+        return $this->_datatree->exists($group);
+    }
+
+    /**
+     * Returns a tree of the parents of a child group.
+     *
+     * @param integer $gid  The id of the child group.
+     *
+     * @return array  The group parents tree, with groupnames as the keys.
+     */
+    function getGroupParents($gid)
+    {
+        if (!isset($this->_parentTree[$gid])) {
+            $name = $this->getGroupName($gid);
+            $parents = $this->_datatree->getParents($name);
+            if (is_a($parents, 'PEAR_Error')) {
+                return $parents;
+            }
+            $this->_parentTree[$gid] = $parents;
+        }
+
+        return $this->_parentTree[$gid];
+    }
+
+    /**
+     * Returns the single parent ID of the given group.
+     *
+     * @param integer $gid  The DataTree ID of the child group.
+     *
+     * @return integer  The parent of the given group.
+     */
+    function getGroupParent($gid)
+    {
+        if (!isset($this->_groupParents[$gid])) {
+            $parent = $this->_datatree->getParentById($gid);
+            if (is_a($parent, 'PEAR_Error')) {
+                return $parent;
+            }
+            $this->_groupParents[$gid] = $parent;
+        }
+
+        return $this->_groupParents[$gid];
+    }
+
+    /**
+     * Returns a flat list of the parents of a child group
+     *
+     * @param integer $gid  The id of the group.
+     *
+     * @return array  A flat list of all of the parents of $group, hashed in
+     *                $id => $name format.
+     */
+    function getGroupParentList($gid)
+    {
+        if (!isset($this->_groupParentList[$gid])) {
+            $parents = $this->_datatree->getParentList($gid);
+            if (is_a($parents, 'PEAR_Error')) {
+                return $parents;
+            }
+            $this->_groupParentList[$gid] = $parents;
+        }
+
+        return $this->_groupParentList[$gid];
+    }
+
+    /**
+     * Returns a list of all groups, in the format id => groupname.
+     *
+     * @param boolean $refresh  If true, the cached value is ignored and the
+     *                          group list is refreshed from the group backend.
+     *
+     * @return array  ID => groupname hash.
+     */
+    function listGroups($refresh = false)
+    {
+        if ($refresh || !isset($this->_groupList)) {
+            $this->_groupList = $this->_datatree->get(DATATREE_FORMAT_FLAT, GROUP_ROOT, true);
+            unset($this->_groupList[GROUP_ROOT]);
+        }
+
+        return $this->_groupList;
+    }
+
+    /**
+     * Get a list of every user that is a part of this group ONLY.
+     *
+     * @param integer $gid  The ID of the group.
+     *
+     * @return array  The user list.
+     */
+    function listUsers($gid)
+    {
+        $groupOb = &$this->getGroupById($gid);
+        if (is_a($groupOb, 'PEAR_Error')) {
+            return $groupOb;
+        }
+
+        if (!isset($groupOb->data['users']) ||
+            !is_array($groupOb->data['users'])) {
+            return array();
+        }
+
+        return array_keys($groupOb->data['users']);
+    }
+
+    /**
+     * Get a list of every user that is part of the specified group
+     * and any of its subgroups.
+     *
+     * @param integer $group  The ID of the parent group.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers($gid)
+    {
+        if (!isset($this->_subGroups[$gid])) {
+            // Get a list of every group that is a sub-group of $group.
+            $groups = $this->_datatree->get(DATATREE_FORMAT_FLAT, $this->getGroupName($gid), true);
+            if (is_a($groups, 'PEAR_Error')) {
+                return $groups;
+            }
+            $this->_subGroups[$gid] = array_keys($groups);
+        }
+
+        $users = array();
+        foreach ($this->_subGroups[$gid] as $groupId) {
+            $users = array_merge($users, $this->listUsers($groupId));
+        }
+        return array_values(array_flip(array_flip($users)));
+    }
+
+    /**
+     * Get a list of every group that $user is in.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        if (!isset($this->_userGroups[$user])) {
+            $criteria = array(
+                'AND' => array(
+                    array('field' => 'name', 'op' => '=', 'test' => 'user'),
+                    array('field' => 'key', 'op' => '=', 'test' => $user)));
+            $groups = $this->_datatree->getByAttributes($criteria);
+
+            if (is_a($groups, 'PEAR_Error')) {
+                return $groups;
+            }
+
+            if ($parentGroups) {
+                foreach ($groups as $id => $g) {
+                    $parents = $this->_datatree->getParentList($id);
+                    if (is_a($parents, 'PEAR_Error')) {
+                        return $parents;
+                    }
+                    $groups += $parents;
+                }
+            }
+
+            $this->_userGroups[$user] = $groups;
+        }
+
+        return $this->_userGroups[$user];
+    }
+
+    /**
+     * Say if a user is a member of a group or not.
+     *
+     * @param string $user        The name of the user.
+     * @param integer $gid        The ID of the group.
+     * @param boolean $subgroups  Return true if the user is in any subgroups
+     *                            of group with ID $gid, also.
+     *
+     * @return boolean
+     */
+    function userIsInGroup($user, $gid, $subgroups = true)
+    {
+        if (!$this->exists($this->getGroupName($gid))) {
+            return false;
+        } elseif ($subgroups) {
+            $groups = $this->getGroupMemberships($user, true);
+            if (is_a($groups, 'PEAR_Error')) {
+                Horde::logMessage($groups, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return false;
+            }
+
+            return !empty($groups[$gid]);
+        } else {
+            $users = $this->listUsers($gid);
+            if (is_a($users, 'PEAR_Error')) {
+                Horde::logMessage($users, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return false;
+            }
+            return in_array($user, $users);
+        }
+    }
+
+    /**
+     * Returns the nesting level of the given group. 0 is returned for any
+     * object directly below GROUP_ROOT.
+     *
+     * @param integer $gid  The DataTree ID of the group.
+     *
+     * @return The DataTree level of the group.
+     */
+    function getLevel($gid)
+    {
+        $name = $this->getGroupName($gid);
+        return substr_count($name, ':');
+    }
+
+    /**
+     * Stores the object in the session cache.
+     */
+    function shutdown()
+    {
+        $session = Horde_SessionObjects::singleton();
+        $session->overwrite('horde_group', $this, false);
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_datatree']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Attempts to return a concrete Group instance based on $driver.
+     *
+     * @param mixed $driver  The type of concrete Group subclass to return.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return Group  The newly created concrete Group instance, or a
+     *                PEAR_Error object on an error.
+     */
+    public static function factory($driver = '', $params = null)
+    {
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('group', $driver);
+        }
+
+        $class = Group::_loadDriver($driver);
+        if (class_exists($class)) {
+            $group = new $class($params);
+        } else {
+            $group = PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+
+        return $group;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete Group instance.
+     * It will only create a new instance if no Group instance
+     * currently exists.
+     *
+     * This method must be invoked as: $var = &Group::singleton()
+     *
+     * @return Group  The concrete Group reference, or false on an error.
+     */
+    public static function singleton()
+    {
+        static $group;
+
+        if (isset($group)) {
+            return $group;
+        }
+
+        $group_driver = null;
+        $group_params = null;
+        $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        if ($auth->hasCapability('groups')) {
+            $group_driver = $auth->getDriver();
+            $group_params = $auth;
+        } elseif (!empty($GLOBALS['conf']['group']['driver']) &&
+                  $GLOBALS['conf']['group']['driver'] != 'datatree') {
+            $group_driver = $GLOBALS['conf']['group']['driver'];
+            $group_params = Horde::getDriverConfig('group', $group_driver);
+        }
+
+        Group::_loadDriver($group_driver);
+
+        $group = null;
+        if (!empty($GLOBALS['conf']['group']['cache'])) {
+            $session = Horde_SessionObjects::singleton();
+            $group = $session->query('horde_group');
+        }
+
+        if (!$group) {
+            $group = Group::factory($group_driver, $group_params);
+        }
+
+        if (!empty($GLOBALS['conf']['group']['cache'])) {
+            register_shutdown_function(array(&$group, 'shutdown'));
+        }
+
+        return $group;
+    }
+
+    protected static function _loadDriver($driver)
+    {
+        if (!$driver) {
+            $class = 'Group';
+        } else {
+            $driver = basename($driver);
+            $class = 'Group_' . $driver;
+            if (!class_exists($class)) {
+                include 'Horde/Group/' . $driver . '.php';
+            }
+        }
+
+        return $class;
+    }
+
+}
+
+/**
+ * Extension of the DataTreeObject class for storing Group information
+ * in the Categories driver. If you want to store specialized Group
+ * information, you should extend this class instead of extending
+ * DataTreeObject directly.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 2.1
+ * @package Horde_Group
+ */
+class DataTreeObject_Group extends DataTreeObject {
+
+    /**
+     * The Group object which this group is associated with - needed
+     * for updating data in the backend to make changes stick, etc.
+     *
+     * @var Group
+     */
+    var $_groupOb;
+
+    /**
+     * This variable caches the users added or removed from the group
+     * for History logging of user-groups relationship.
+     *
+     * @var array
+     */
+    var $_auditLog = array();
+
+    /**
+     * The DataTreeObject_Group constructor. Just makes sure to call
+     * the parent constructor so that the group's name is set
+     * properly.
+     *
+     * @param string $name  The name of the group.
+     */
+    function DataTreeObject_Group($name)
+    {
+        parent::DataTreeObject($name);
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['datatree'], $properties['_groupOb']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Associates a Group object with this group.
+     *
+     * @param Group $groupOb  The Group object.
+     */
+    function setGroupOb(&$groupOb)
+    {
+        $this->_groupOb = &$groupOb;
+    }
+
+    /**
+     * Fetch the ID of this group
+     *
+     * @return string The group's ID
+     */
+    function getId()
+    {
+        return $this->_groupOb->getGroupId($this);
+    }
+
+    /**
+     * Save any changes to this object to the backend permanently.
+     */
+    function save()
+    {
+        return $this->_groupOb->updateGroup($this);
+
+    }
+
+    /**
+     * Adds a user to this group, and makes sure that the backend is
+     * updated as well.
+     *
+     * @param string $username The user to add.
+     */
+    function addUser($username, $update = true)
+    {
+        $this->data['users'][$username] = 1;
+        $this->_auditLog[$username] = 'addUser';
+        if ($update && $this->_groupOb->exists($this->getName())) {
+            return $this->save();
+        }
+    }
+
+    /**
+     * Removes a user from this group, and makes sure that the backend
+     * is updated as well.
+     *
+     * @param string $username The user to remove.
+     */
+    function removeUser($username, $update = true)
+    {
+        unset($this->data['users'][$username]);
+        $this->_auditLog[$username] = 'deleteUser';
+        if ($update) {
+            return $this->save();
+        }
+    }
+
+    /**
+     * Get a list of every user that is a part of this group
+     * (and only this group)
+     *
+     * @return array  The user list
+     */
+    function listUsers()
+    {
+        return $this->_groupOb->listUsers($this->getId());
+    }
+
+    /**
+     * Get a list of every user that is a part of this group and
+     * any of it's subgroups
+     *
+     * @return array  The complete user list
+     */
+    function listAllUsers()
+    {
+        return $this->_groupOb->listAllUsers($this->getId());
+    }
+
+    /**
+     * Get all the users recently added or removed from the group.
+     */
+    function getAuditLog()
+    {
+        return $this->_auditLog;
+    }
+
+    /**
+     * Clears the audit log. To be called after group update.
+     */
+    function clearAuditLog()
+    {
+        $this->_auditLog = array();
+    }
+
+    /**
+     * Map this object's attributes from the data array into a format
+     * that we can store in the attributes storage backend.
+     *
+     * @return array  The attributes array.
+     */
+    function _toAttributes()
+    {
+        // Default to no attributes.
+        $attributes = array();
+
+        // Loop through all users, if any.
+        if (isset($this->data['users']) && is_array($this->data['users']) && count($this->data['users'])) {
+            foreach ($this->data['users'] as $user => $active) {
+                $attributes[] = array('name' => 'user',
+                                      'key' => $user,
+                                      'value' => $active);
+            }
+        }
+        $attributes[] = array('name' => 'email',
+                              'key' => '',
+                              'value' => $this->get('email'));
+
+        return $attributes;
+    }
+
+    /**
+     * Take in a list of attributes from the backend and map it to our
+     * internal data array.
+     *
+     * @param array $attributes  The list of attributes from the
+     *                           backend (attribute name, key, and value).
+     */
+    function _fromAttributes($attributes)
+    {
+        // Initialize data array.
+        $this->data['users'] = array();
+
+        foreach ($attributes as $attr) {
+            if ($attr['name'] == 'user') {
+                $this->data['users'][$attr['key']] = $attr['value'];
+            } else {
+                $this->data[$attr['name']] = $attr['value'];
+            }
+        }
+    }
+
+}
diff --git a/framework/Group/Group/contactlists.php b/framework/Group/Group/contactlists.php
new file mode 100644 (file)
index 0000000..4dae041
--- /dev/null
@@ -0,0 +1,726 @@
+<?php
+/**
+ * The Group_contactlists class provides a groups system based on Turba
+ * contact lists. Only SQL sources are supported.
+ *
+ * $Horde: framework/Group/Group/contactlists.php,v 1.6 2009-11-22 18:15:21 slusarz Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_Group
+ */
+class Group_contactlists extends Group {
+
+    /**
+     * A cache object
+     *
+     * @var Horde_Cache object
+     */
+    var $_cache = null;
+
+    /**
+     * Handles for the  database connections. Need one for each possible source.
+     *
+     * @var DB
+     */
+    var $_db = array();
+
+    /**
+     * Local copy of available address book sources that the group driver can
+     * use.
+     *
+     * @var array of Turba's cfgSource style entries.
+     */
+    var $_sources = array();
+
+    /**
+     * Local cache of retreived group entries from Turba storage.
+     *
+     * @var unknown_type
+     */
+    var $_listEntries = array();
+
+    /**
+     * Constructor.
+     */
+    function Group_contactlists($params)
+    {
+        // Get a list of all available Turba sources
+        $turba_sources = Horde::loadConfiguration('sources.php',
+                                                  'cfgSources', 'turba');
+
+        // We only support sql type sources.
+        foreach ($turba_sources as $key => $source) {
+            if ($source['type'] == 'sql') {
+                $this->_sources[$key] = $source;
+            }
+        }
+
+        $this->_cache = &Horde_Cache::singleton(
+            $GLOBALS['conf']['cache']['driver'], Horde::getDriverConfig('cache', $GLOBALS['conf']['cache']['driver']));
+    }
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+    }
+
+    /**
+     * Stores the object in the session cache.
+     */
+    function shutdown()
+    {
+    }
+
+    /**
+     * Returns a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's name.
+     *
+     * @return PEAR_Error  This functionality is not supported in this driver.
+     */
+    function &newGroup($name, $parent = GROUP_ROOT)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Returns a Group object corresponding to the named group,
+     * with the users and other data retrieved appropriately.
+     *
+     * This is deprecated. Use getGroupById instead.
+     *
+     * @param string $name The name of the group to retrieve.
+     */
+    function &getGroup($name)
+    {
+        return PEAR::raiseError(_("Deprecated. Use getGroupById() instead."));
+    }
+
+    /**
+     * Returns a ContactListObject_Group object corresponding to the given unique
+     * ID, with the users and other data retrieved appropriately.
+     *
+     * @param integer $cid  The unique ID of the group to retrieve.
+     */
+    function &getGroupById($gid)
+    {
+
+        if (!empty($this->_groupCache[$gid])) {
+            return $this->_groupCache[$gid];
+        }
+        list($source, $id) = explode(':', $gid);
+        $entry = $this->_retrieveListEntry($gid);
+        if (is_a($entry, 'PEAR_Error')) {
+            return $entry;
+        } elseif (empty($entry)) {
+            return PEAR::raiseError($gid . ' does not exist');
+        }
+
+        $users = $this->_getAllMembers($gid);
+        if (is_a($users, 'PEAR_Error')) {
+            return $users;
+        }
+
+        $group = new ContactListObject_Group($entry[$this->_sources[$source]['map'][$this->_sources[$source]['list_name_field']]]);
+        $group->id = $gid;
+        $group->data['email'] = $entry[$this->_sources[$source]['map']['email']];
+        var_dump($users);
+        if (!empty($users)) {
+            $group->data['users'] = array_values($users);
+        }
+
+        $group->setGroupOb($this);
+        $this->_groupCache[$gid] = &$group;
+
+        return $group;
+    }
+
+    /**
+     * Adds a group to the groups system. The group must first be created with
+     * Group::newGroup(), and have any initial users added to it, before this
+     * function is called.
+     *
+     * @param ContactListObjectObject_Group $group  The new group object.
+     *
+     * @return PEAR_Error - unsupported
+     */
+    function addGroup(&$group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Stores updated data - users, etc. - of a group to the backend system.
+     *
+     * @param ContactListObject_Group $group  The group to update.
+     */
+    function updateGroup($group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Removes a group from the groups system permanently.
+     *
+     * @param ContactListObject_Group $group  The group to remove.
+     * @param boolean $force          Force to remove every child.
+     *
+     * @return PEAR_Error - unsupported.
+     */
+    function removeGroup($group, $force = false)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Retrieves the name of a group.
+     *
+     * @param integer|ContactListObject_Group $gid  The id of the group or the
+     *                                      group object to retrieve the
+     *                                      name for.
+     *
+     * @return string  The group's name.
+     */
+    function getGroupName($gid)
+    {
+        static $beenHere;
+
+        if (strpos($gid, ':') === false) {
+            return PEAR::raiseError(sprintf(_("Group %s not found."), $gid));
+        }
+
+        if (is_a($gid, 'ContactListObject_Group')) {
+            $gid = $gid->getId();
+        }
+        if (!empty($this->_listEntries[$gid])) {
+            list($source, $id) = explode(':', $gid);
+            $beenHere = false;
+            return $this->_listEntries[$gid][$this->_sources[$source]['map'][$this->_sources[$source]['list_name_field']]];
+        }
+
+        $result = $this->_retrieveListEntry($gid);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // We should have the information cached now, try again..but protect
+        // against anything nasty...
+        if (!$beenHere) {
+            $beenHere = true;
+            return $this->getGroupName($gid);
+        }
+
+        return PEAR::raiseError(sprintf(_("Group %s not found."), $gid));
+    }
+
+    /**
+     * Strips all parent references off of the given group name.
+     *
+     * Not used in this driver...group display names are ONLY for display.
+     *
+     * @param string $group  Name of the group.
+     *
+     * @return The name of the group without parents.
+     */
+    function getGroupShortName($group)
+    {
+       return $group;
+    }
+
+    /**
+     * Retrieves the ID of a group, given the group object.
+     * Here for BC. Kinda silly, since if we have the object, we can just call
+     * getId() ourselves.
+     *
+     * @param ContactListObject_Group $group  The group object to retrieve the
+     *                                        ID for.
+     *
+     * @return integer  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        if (is_a($group, 'ContactListObject_Group')) {
+            return $group->getId();
+        }
+
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Check if a group exists in the system.
+     *
+     * This must either be a noop or we need to somehow "uniqueify" the
+     * list's display name?
+     *
+     * @param string $group  The group name to check.
+     *
+     * @return boolean  True if the group exists, false otherwise.
+     */
+    function exists($group)
+    {
+        return true;
+    }
+
+    /**
+     * Returns a tree of the parents of a child group.
+     *
+     * @param integer $gid  The id of the child group.
+     *
+     * @return array  The group parents tree, with groupnames as the keys.
+     */
+    function getGroupParents($gid)
+    {
+        return array();
+    }
+
+
+    /**
+     * Returns the single parent ID of the given group.
+     *
+     * @param integer $gid  The ID of the child group.
+     *
+     * @return integer  The parent of the given group.
+     */
+    function getGroupParent($gid)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Returns a flat list of the parents of a child group
+     *
+     * @param integer $gid  The id of the group.
+     *
+     * @return array  A flat list of all of the parents of $group, hashed in
+     *                $id => $name format.
+     */
+    function getGroupParentList($gid)
+    {
+        return array();
+    }
+
+    /**
+     * Returns a list of all groups, in the format id => groupname.
+     * The groups returned represent only the groups visible to the current user
+     * only.
+     *
+     * @param boolean $refresh  If true, the cached value is ignored and the
+     *                          group list is refreshed from the group backend.
+     *
+     * @return array  ID => groupname hash.
+     */
+    function listGroups($refresh = false)
+    {
+        if (isset($this->_groupList) && !$refresh) {
+            return $this->_groupList;
+        }
+
+        // First, make sure we are connected to all sources
+        $this->_connect();
+
+        $groups = array();
+        $owners = array();
+        foreach ($this->_sources as $key => $source) {
+            if ($source['use_shares']) {
+                if (empty($contact_shares)) {
+                    $scope = $GLOBALS['registry']->hasInterface('contacts');
+                    $shares = Horde_Share::singleton($scope);
+                    $this->_contact_shares = $shares->listShares(Horde_Auth::getAuth(), Horde_Perms::SHOW, Horde_Auth::getAuth());
+                }
+                // Contruct a list of owner ids to use
+                foreach ($this->_contact_shares as $id => $share) {
+                    $params = @unserialize($share->get('params'));
+                    if ($params['source'] == $key) {
+                        $owners[] = $params['name'];
+                    }
+                }
+            } else {
+                $owners = array(Horde_Auth::getAuth());
+            }
+            $owner_ids = array();
+            foreach ($owners as $owner) {
+                $owner_ids[] = $this->_db[$key]->quote($owner);
+            }
+            $sql = 'SELECT ' . $source['map']['__key'] . ', ' . $source['map'][$source['list_name_field']]
+                . '  FROM ' . $source['params']['table'] . ' WHERE '
+                . $source['map']['__type'] . ' = \'Group\' AND '
+                . $source['map']['__owner'] . ' IN (' . implode(',', $owner_ids ) . ')';
+
+           $results = $this->_db[$key]->getAssoc($sql);
+           foreach ($results as $id => $name) {
+               $groups[$key . ':' . $id] = $name;
+           }
+        }
+        $this->_groupList = $groups;
+
+        return $this->_groupList;
+    }
+
+    /**
+     * Get a list of every user that is part of the specified group
+     * and any of its subgroups.
+     *
+     * @param integer $group  The ID of the parent group.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers($gid)
+    {
+        $members = $this->_getAllMembers($gid, true);
+        if (is_a($members, 'PEAR_Error')) {
+            return $members;
+        }
+
+        return array_values($members);
+    }
+
+    /**
+     * Returns a hash representing the list entry. Items are keyed by the
+     * backend specific keys.
+     *
+     * @param string $gid  The group id
+     * @return array | PEAR_Error
+     */
+    function _retrieveListEntry($gid)
+    {
+        if (!empty($this->_listEntries[$gid])) {
+            return $this->_listEntries[$gid];
+        }
+
+        list($source, $id) = explode(':', $gid);
+        if (empty($this->_sources[$source])) {
+            return array();
+        }
+
+        $this->_connect($source);
+        $sql = 'SELECT ' . $this->_sources[$source]['map']['__members'] . ','
+            . $this->_sources[$source]['map']['email'] . ','
+            . $this->_sources[$source]['map'][$this->_sources[$source]['list_name_field']]
+            . ' from ' . $this->_sources[$source]['params']['table'] . ' WHERE '
+            . $this->_sources[$source]['map']['__key'] . ' = ' . $this->_db[$source]->quote($id);
+
+        $results = $this->_db[$source]->getRow($sql,array(), DB_FETCHMODE_ASSOC);
+        if (is_a($results, 'PEAR_Error')) {
+            Horde::logMessage($results, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+        $this->_listEntries[$gid] = $results;
+
+        return $results;
+
+    }
+
+    /**
+     * TODO
+     */
+    function _getAllMembers($gid, $subGroups = false)
+    {
+        if (empty($gid) || strpos($gid, ':') === false) {
+            return PEAR::raiseError(sprintf(_("Unsupported group id: %s"), $gid));
+        }
+
+        list($source, $id) = explode(':', $gid);
+        $entry = $this->_retrieveListEntry($gid);
+        if (is_a($entry, 'PEAR_Error')) {
+            return $entry;
+        }
+        $members = @unserialize($entry[$this->_sources[$source]['map']['__members']]);
+        $users = array();
+
+        // TODO: optimize this to only query each table once
+        foreach ($members as $member) {
+
+            // Is this member from the same source or a different one?
+            if (strpos($member, ':') !== false) {
+                list($newSource, $uid) = explode(':', $member);
+                if (!empty($this->_contact_shares[$newSource])) {
+                    $params = @unserialize($this->_contact_shares[$newSource]->get('params'));
+                    $newSource = $params['source'];
+                    $member = $uid;
+                    $this->_connect($newSource);
+                } elseif (empty($this->_sources[$newSource])) {
+                    // Last chance, it's not in one of our non-share sources
+                    continue;
+                }
+            } else {
+                // Same source
+                $newSource = $source;
+            }
+
+            $sql = 'SELECT ' . $this->_sources[$newSource]['map']['email']
+                . ', ' . $this->_sources[$newSource]['map']['__type']
+                . ' FROM ' . $this->_sources[$newSource]['params']['table']
+                . ' WHERE ' . $this->_sources[$newSource]['map']['__key']
+                . ' = ' . $this->_db[$newSource]->quote($member);
+
+            $results = $this->_db[$newSource]->getRow($sql);
+            if (is_a($results, 'PEAR_Error')) {
+                Horde::logMessage($results, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $results;
+            }
+
+            // Sub-Lists are treated as sub groups the best that we can...
+            if ($subGroups && $results[1] == 'Group') {
+                $this->_subGroups[$gid] = $newSource . ':' . $member;
+                $users = array_merge($users, $this->_getAllMembers($newSource . ':' . $member));
+            }
+            if (strlen($results[0])) {
+                // use a key to dump dups
+                $users[$results[0]] = $results[0];
+            }
+        }
+
+        return $users;
+    }
+
+    /**
+     * Returns ALL contact lists present in ALL sources that this driver knows
+     * about.
+     *
+     */
+    function _listAllLists()
+    {
+        // Clear the cache - we will rebuild it.
+        $this->_listEntries = array();
+
+        foreach ($this->_sources as $key => $source) {
+            $this->_connect($key);
+            $sql = 'SELECT ' . $source['map']['__key'] . ','
+            . $source['map']['__members'] . ','
+            . $source['map']['email'] . ','
+            . $source['map'][$source['list_name_field']]
+            . ' FROM ' . $source['params']['table'] . ' WHERE '
+            . $source['map']['__type'] . ' = \'Group\'';
+
+           $results = $this->_db[$key]->query($sql);
+           if (is_a($results, 'PEAR_Error')) {
+               return $results;
+           }
+           while ($row = $results->fetchRow(DB_FETCHMODE_ASSOC)) {
+                $this->_listEntries[$key . ':' . $row[$source['map']['__key']]] = $row;
+           }
+        }
+
+        return $this->_listEntries;
+    }
+
+    /**
+     * Get a list of every group that $user is in.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        if ($memberships = $this->_cache->get('Group_contactlists_memberships' . md5($user)) !== false) {
+            return @unserialize($memberships);
+        }
+        $lists = $this->_listAllLists();
+        $memberships = array();
+        foreach (array_keys($lists) as $list) {
+            $members = $this->_getAllMembers($list, $parentGroups);
+            if (!empty($members[$user])) {
+                $memberships[] = $list;
+            }
+        }
+
+        $this->_cache->set('Group_contactlists_memberships' . md5($user), serialize($memberships));
+        return $memberships;
+
+    }
+
+    /**
+     * Say if a user is a member of a group or not.
+     *
+     * @param string $user        The name of the user.
+     * @param integer $gid        The ID of the group.
+     * @param boolean $subgroups  Return true if the user is in any subgroups
+     *                            of group with ID $gid, also.
+     *
+     * @return boolean
+     */
+    function userIsInGroup($user, $gid, $subgroups = true)
+    {
+        if (isset($_SESSION['horde']['groups']['i'][$user][$subgroups][$gid])) {
+            return $_SESSION['horde']['groups']['i'][$user][$subgroups][$gid];
+        }
+
+        $users = $this->_getAllMembers($gid, $subgroups);
+        if (is_a($users, 'PEAR_Error')) {
+            Horde::logMessage($users, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+        $result = !empty($users[$user]);
+        $_SESSION['horde']['groups']['i'][$user][$subgroups][$gid] = (bool)$result;
+        return (bool)$result;
+    }
+
+    /**
+     * Attempts to open a persistent connection to the sql server.
+     *
+     * @return boolean  True on success.
+     * @throws Horde_Exception
+     */
+    function _connect($source = null)
+    {
+        if (!is_null($source) && !empty($this->_db[$source])) {
+            return true;
+        }
+        /* Connect to the sql server using the supplied parameters. */
+        require_once 'DB.php';
+
+        if (is_null($source)) {
+            $sources = array_keys($this->_sources);
+        } else {
+            $sources = array($source);
+        }
+
+        foreach ($sources as $source) {
+            if (empty($this->_db[$source])) {
+                $this->_db[$source] = &DB::connect($this->_sources[$source]['params'],
+                    array('persistent' => !empty($this->_sources[$source]['params']['persistent'])));
+                if (is_a($this->_db[$source], 'PEAR_Error')) {
+                    throw new Horde_Exception($this->_db[$source]);
+                }
+
+                /* Set DB portability options. */
+                switch ($this->_db[$source]->phptype) {
+                case 'mssql':
+                    $this->_db[$source]->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                    break;
+                default:
+                    $this->_db[$source]->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+                }
+            }
+        }
+
+        return true;
+    }
+
+}
+
+/**
+ * Extension of the DataTreeObject_Group class for storing Group information.
+ *
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @since   Horde 4.0
+ * @package Horde_Group
+ */
+class ContactListObject_Group extends DataTreeObject_Group {
+
+    /**
+     * The unique name of this object.
+     * These names have the same requirements as other object names - they must
+     * be unique, etc.
+     *
+     * @var string
+     */
+    var $name;
+
+    /**
+     * The unique name of this object.
+     * These names have the same requirements as other object names - they must
+     * be unique, etc.
+     *
+     * @var integer
+     */
+    var $id;
+
+    /**
+     * Key-value hash that will be serialized.
+     *
+     * @see getData()
+     * @var array
+     */
+    var $data = array();
+
+    /**
+     * Constructor.
+     *
+     * @param string $name  The name of the group.
+     */
+    function ContactListObject_Group($name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Gets the ID of this object.
+     *
+     * @return string  The object's ID.
+     */
+    function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Gets the name of this object.
+     *
+     * @return string The object name.
+     */
+    function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Gets one of the attributes of the object, or null if it isn't defined.
+     *
+     * @param string $attribute  The attribute to get.
+     *
+     * @return mixed  The value of the attribute, or null.
+     */
+    function get($attribute)
+    {
+        return isset($this->data[$attribute])
+            ? $this->data[$attribute]
+            : null;
+    }
+
+    /**
+     * Sets one of the attributes of the object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     */
+    function set($attribute, $value)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Save group
+     */
+    function save()
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    function removeUser($username, $update = true)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+    function addUser($username, $update = true)
+    {
+       return PEAR::raiseError(_("Unsupported"));
+    }
+}
diff --git a/framework/Group/Group/hooks.php b/framework/Group/Group/hooks.php
new file mode 100644 (file)
index 0000000..33e79ed
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * The Group_hooks:: class provides the Horde groups system with the
+ * addition of adding support for hook functions to define if a user
+ * is in a group.
+ *
+ * $Horde: framework/Group/Group/hooks.php,v 1.23 2009/01/06 17:49:18 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason Rust <jrust@rustyparts.com>
+ * @since   Horde 3.0
+ * @package Horde_Group
+ */
+class Group_hooks extends Group {
+
+    var $_hookFunction = false;
+
+    /**
+     * Constructor.
+     */
+    function Group_hooks($params)
+    {
+        parent::Group($params);
+        Horde::loadConfiguration('hooks.php', null, 'horde');
+        $this->_hookFunction = function_exists('_group_hook');
+    }
+
+    /**
+     * Get a list of every group that $user is in.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        $memberships = parent::getGroupMemberships($user, $parentGroups);
+        if (!$this->_hookFunction) {
+            return $memberships;
+        }
+
+        $groups = $this->listGroups();
+        foreach ($groups as $gid => $groupName) {
+            if (empty($memberships[$gid]) && _group_hook($groupName, $user)) {
+                $memberships += array($gid => $groupName);
+            }
+
+            if ($parentGroups) {
+                $parents = $this->getGroupParentList($gid);
+                if (is_a($parents, 'PEAR_Error')) {
+                    return $parents;
+                }
+
+                $memberships += $parents;
+            }
+        }
+
+        return $memberships;
+    }
+
+    /**
+     * Say if a user is a member of a group or not.
+     *
+     * @param string  $user       The name of the user.
+     * @param integer $gid        The ID of the group.
+     * @param boolean $subgroups  Return true if the user is in any subgroups
+     *                            of $group, also.
+     *
+     * @return boolean
+     */
+    function userIsInGroup($user, $gid, $subgroups = true)
+    {
+        $inGroup = ($this->_hookFunction && _group_hook($this->getGroupName($gid), $user));
+        return ($inGroup || parent::userIsInGroup($user, $gid, $subgroups));
+    }
+
+}
diff --git a/framework/Group/Group/kolab.php b/framework/Group/Group/kolab.php
new file mode 100644 (file)
index 0000000..9656319
--- /dev/null
@@ -0,0 +1,499 @@
+<?php
+
+require_once 'Horde/Group/ldap.php';
+require_once 'Horde/LDAP.php';
+
+/**
+ * The Group_kolab class provides a Kolab backend for the Horde groups
+ * system.
+ *
+ * FIXME: A better solution would be to let this class rely on
+ *        Horde/Kolab/LDAP.php.
+ *
+ * $Horde: framework/Group/Group/kolab.php,v 1.7 2009/07/09 08:17:55 slusarz Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Group
+ */
+class Group_kolab extends Group_ldap {
+
+    /**
+     * A marker for fatal errors
+     */
+    var $_error;
+
+    /**
+     * Constructor.
+     */
+    function Group_kolab($params)
+    {
+        if (!function_exists('ldap_connect')) {
+            $this->_error = PEAR::raiseError(_("The Kolab group driver requires LDAP support."));
+        }
+
+        $this->_params = array();
+        $this->_params['hostspec'] = $GLOBALS['conf']['kolab']['ldap']['server'];
+        $this->_params['basedn'] = $GLOBALS['conf']['kolab']['ldap']['basedn'];
+        $this->_params['binddn'] = $GLOBALS['conf']['kolab']['ldap']['phpdn'];
+        $this->_params['password'] = $GLOBALS['conf']['kolab']['ldap']['phppw'];
+        $this->_params['version'] = 3;
+        $this->_params['gid'] = 'cn';
+        $this->_params['memberuid'] = 'member';
+        $this->_params['attrisdn'] = true;
+        $this->_params['filter_type'] = 'objectclass';
+        $this->_params['objectclass'] = 'kolabGroupOfNames';
+        $this->_params['newgroup_objectclass'] = 'kolabGroupOfNames';
+
+        $this->_filter = 'objectclass=' . $this->_params['objectclass'];
+
+        $this->__wakeup();
+    }
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+        foreach (array_keys($this->_groupCache) as $name) {
+            $this->_groupCache[$name]->setGroupOb($this);
+        }
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_datatree'], $properties['_ds']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+
+    /**
+     * Returns a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's name.
+     *
+     * @return Kolab_Group  A new group object.
+     */
+    function &newGroup($name)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Adds a group to the groups system. The group must first be created with
+     * Group::newGroup(), and have any initial users added to it, before this
+     * function is called.
+     *
+     * @param Kolab_Group $group  The new group object.
+     */
+    function addGroup($group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Stores updated data - users, etc. - of a group to the backend system.
+     *
+     * @param Kolab_Group $group  The group to update.
+     */
+    function updateGroup($group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Removes a group from the groups system permanently.
+     *
+     * @param Kolab_Group $group  The group to remove.
+     * @param boolean $force               Force to remove every child.
+     */
+    function removeGroup($group, $force = false)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Return a Kolab_Group object corresponding to the given dn, with the
+     * users and other data retrieved appropriately.
+     *
+     * @param string $dn  The dn of the group to retrieve.
+     *
+     * @return Kolab_Group  The requested group.
+     */
+    function &getGroupById($dn)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$dn])) {
+
+            if (is_a($this->_error, 'PEAR_Error')) {
+                return $this->_error;
+            }
+
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $search = @ldap_search($this->_ds, $dn, $this->_filter);
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return PEAR::raiseError(_("Empty result"));
+            }
+
+            $attributes = array();
+            for ($i = 0; $i < $result[0]['count']; $i++) {
+                if ($result[0][$result[0][$i]]['count'] > 1) {
+                    $attributes[$result[0][$i]] = array();
+                    for ($j = 0; $j < $result[0][$result[0][$i]]['count']; $j++) {
+                        $attributes[$result[0][$i]][] = $result[0][$result[0][$i]][$j];
+                    }
+                } else {
+                    $attributes[$result[0][$i]] = $result[0][$result[0][$i]][0];
+                }
+            }
+            $attributes['dn'] = $result[0]['dn'];
+
+            $group = new Kolab_Group($this->getGroupName($dn));
+            $group->_fromAttributes($attributes);
+            $group->setGroupOb($this);
+            $cache[$dn] = $group;
+        }
+
+        return $cache[$dn];
+    }
+
+
+    /**
+     * Retrieve the ID of the given group.
+     *
+     * NOTE: If given a group name, this function can be unreliable if more
+     * than one group exists with the same name.
+     *
+     * @param mixed $group   LDAP_Group object, or a group name (string)
+     *
+     * @return string  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        static $cache = array();
+
+        if (is_a($group, 'Kolab_Group')) {
+            return $group->getDn();
+        }
+
+        if (!isset($cache[$group])) {
+
+            if (is_a($this->_error, 'PEAR_Error')) {
+                return $this->_error;
+            }
+
+            $this->_connect();
+            $search = @ldap_search($this->_ds, $this->_params['basedn'],
+                                   $this->_params['gid'] . '=' . $group,
+                                   array($this->_params['gid']));
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return PEAR::raiseError(_("Empty result"));
+            }
+            $cache[$group] = $result[0]['dn'];
+        }
+
+        return $cache[$group];
+    }
+
+    /**
+     * Get a list of the parents of a child group.
+     *
+     * @param string $dn  The fully qualified group dn
+     *
+     * @return array  Nested array of parents
+     */
+    function getGroupParents($dn)
+    {
+        return array();
+    }
+
+    /**
+     * Get the parent of the given group.
+     *
+     * @param string $dn  The dn of the child group.
+     *
+     * @return string  The dn of the parent group.
+     */
+    function getGroupParent($dn)
+    {
+        return null;
+    }
+
+    /**
+     * Get a list of parents all the way up to the root object for the given
+     * group.
+     *
+     * @param string $dn  The dn of the group.
+     *
+     * @return array  A flat list of all of the parents of the given group,
+     *                hashed in $dn => $name format.
+     */
+    function getGroupParentList($dn)
+    {
+        return array();
+    }
+
+    /**
+     * Tries to find a DN for a given kolab mail address.
+     *
+     * @param string $mail  The mail address to search for.
+     *
+     * @return string  The corresponding dn or false.
+     */
+    function dnForMail($mail)
+    {
+        $filter = '(&(objectclass=kolabInetOrgPerson)(mail=' . Horde_LDAP::quote($mail) . '))';
+        $search = @ldap_search($this->_ds, $this->_params['basedn'], $filter);
+        if (!$search) {
+            return PEAR::raiseError(_("Could not reach the LDAP server"));
+        }
+        $dn = @ldap_first_entry($this->_ds, $search);
+        if ($dn) {
+            return ldap_get_dn($this->_ds, $dn);
+        }
+        return PEAR::raiseError(sprintf(_("Error searching for user with the email address \"%s\"!"),
+                                        $mail));
+    }
+
+    /**
+     * Get a list of every group that the given user is a member of.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        static $cache = array();
+
+        if (empty($cache[$user])) {
+
+            if (is_a($this->_error, 'PEAR_Error')) {
+                return $this->_error;
+            }
+
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $dn = $this->dnForMail($user);
+            if (is_a($dn, 'PEAR_Error')) {
+                return $dn;
+            }
+
+            // Set up search filter
+            $filter = '(' . $this->_params['memberuid'] . '=' . $dn . ')';
+
+            // Perform search
+            $search = @ldap_search($this->_ds, $this->_params['basedn'], $filter);
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return array();
+            }
+
+            $groups = array();
+            $current_charset = Horde_Nls::getCharset();
+            for ($i = 0; $i < $result['count']; $i++) {
+                $utf8_dn = Horde_String::convertCharset($result[$i]['dn'], 'UTF-8', $current_charset);
+                $groups[$utf8_dn] = $this->getGroupName($utf8_dn);
+            }
+
+            $cache[$user] = $groups;
+        }
+
+        return $cache[$user];
+    }
+
+}
+
+/**
+ *
+ *
+ * @author  Ben Chavet <ben@horde.org>
+ * @since   Horde 3.1
+ * @package Horde_Group
+ */
+class Kolab_Group extends LDAP_Group {
+
+    /**
+     * Constructor.
+     *
+     * @param string $name    The name of this group.
+     * @param string $parent  The dn of the parent of this group.
+     */
+    function Kolab_Group($name, $parent = null)
+    {
+        $this->setName($name);
+    }
+
+    /**
+     * Fetch the ID of this group
+     *
+     * @return string The group's ID
+     */
+    function getId()
+    {
+        return $this->getDn();
+    }
+
+    /**
+     * Save any changes to this object to the backend permanently.
+     */
+    function save()
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Adds a user to this group, and makes sure that the backend is
+     * updated as well.
+     *
+     * @param string $username The user to add.
+     */
+    function addUser($username, $update = true)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+
+    /**
+     * Removes a user from this group, and makes sure that the backend
+     * is updated as well.
+     *
+     * @param string $username The user to remove.
+     */
+    function removeUser($username, $update = true)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Get all the users recently added or removed from the group.
+     */
+    function getAuditLog()
+    {
+        return array();
+    }
+
+    /**
+     * Clears the audit log. To be called after group update.
+     */
+    function clearAuditLog()
+    {
+    }
+
+    /**
+     * Sets the name of this object.
+     *
+     * @param string $name  The name to set this object's name to.
+     */
+    function getDn()
+    {
+        return $this->name . ',' . $GLOBALS['conf']['kolab']['ldap']['basedn'];
+    }
+
+    /**
+     * Take in a list of attributes from the backend and map it to our
+     * internal data array.
+     *
+     * @param array $attributes  The list of attributes from the backend.
+     */
+    function _fromAttributes($attributes = array())
+    {
+        $this->data['users'] = array();
+        foreach ($attributes as $key => $value) {
+            if (Horde_String::lower($key) == 'member') {
+                if (is_array($value)) {
+                    foreach ($value as $user) {
+                        $pattern = '/^cn=([^,]+).*$/';
+                        $results = array();
+                        preg_match($pattern, $user, $results);
+                        if (isset($results[1])) {
+                            $user = $results[1];
+                        }
+                        $this->data['users'][$user] = '1';
+                    }
+                } else {
+                    $pattern = '/^cn=([^,]+).*$/';
+                    $results = array();
+                    preg_match($pattern, $value, $results);
+                    if (isset($results[1])) {
+                        $value = $results[1];
+                    }
+                    $this->data['users'][$value] = '1';
+                }
+            } elseif ($key == 'mail') {
+                $this->data['email'] = $value;
+            } else {
+                $this->data[$key] = $value;
+            }
+        }
+    }
+
+    /**
+     * Map this object's attributes from the data array into a format that
+     * can be stored in an LDAP entry.
+     *
+     * @return array  The entry array.
+     */
+    function _toAttributes()
+    {
+        $attributes = array();
+        foreach ($this->data as $key => $value) {
+            if ($key == 'users') {
+                foreach ($value as $user => $membership) {
+                    $user = 'cn=' . $user . ',' . $GLOBALS['conf']['kolab']['ldap']['basedn'];
+                    $attributes['member'][] = $user;
+                }
+            } elseif ($key == 'email') {
+                if (!empty($value)) {
+                    $attributes['mail'] = $value;
+                }
+            } elseif ($key != 'dn' && $key != 'member') {
+                $attributes[$key] = !empty($value) ? $value : ' ';
+            }
+        }
+
+        return $attributes;
+    }
+
+}
diff --git a/framework/Group/Group/ldap.php b/framework/Group/Group/ldap.php
new file mode 100644 (file)
index 0000000..243c1f5
--- /dev/null
@@ -0,0 +1,859 @@
+<?php
+/**
+ * The Group_ldap class provides an LDAP backend for the Horde groups
+ * system.
+ *
+ * $Horde: framework/Group/Group/ldap.php,v 1.49 2009/07/27 06:15:27 slusarz Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Ben Chavet <ben@horde.org>
+ * @since   Horde 3.1
+ * @package Horde_Group
+ */
+class Group_ldap extends Group {
+
+    /**
+     * LDAP connection handle
+     */
+    var $_ds;
+
+    /**
+     * Local copy of the global $conf['group']['params'] array. Simply
+     * for coding convenience.
+     */
+    var $_params;
+
+    /**
+     * Generated LDAP filter based on the config parameters
+     */
+    var $_filter;
+
+    /**
+     * Constructor.
+     */
+    function Group_ldap($params)
+    {
+        $this->_params = $GLOBALS['conf']['group']['params'];
+
+        $this->_params['gid'] = Horde_String::lower($this->_params['gid']);
+        $this->_params['memberuid'] = Horde_String::lower($this->_params['memberuid']);
+        foreach ($this->_params['newgroup_objectclass'] as $key => $val) {
+            $this->_params['newgroup_objectclass'][$key] = Horde_String::lower($val);
+        }
+
+        /* Generate LDAP search filter. */
+        if (!empty($this->_params['filter'])) {
+            $this->_filter = $this->_params['filter'];
+        } elseif (!is_array($this->_params['objectclass'])) {
+            $this->_filter = 'objectclass=' . $this->_params['objectclass'];
+        } else {
+            $this->_filter = '';
+            foreach ($this->_params['objectclass'] as $objectclass) {
+                $this->_filter = '(&' . $this->_filter;
+                $this->_filter .= '(objectclass=' . $objectclass . '))';
+            }
+        }
+
+        $this->_filter = Horde_String::lower($this->_filter);
+    }
+
+    /**
+     * Connects to the LDAP server.
+     *
+     * @return boolean  True or False based on success of connect and bind.
+     */
+    function _connect()
+    {
+        /* Connect to the LDAP server. */
+        $this->_ds = @ldap_connect($this->_params['hostspec']);
+        if (!$this->_ds) {
+            return PEAR::raiseError(_("Could not reach the LDAP server"));
+        }
+
+        if (!ldap_set_option($this->_ds, LDAP_OPT_PROTOCOL_VERSION,
+                             $this->_params['version'])) {
+            Horde::logMessage(
+                sprintf('Set LDAP protocol version to %d failed: [%d] %s',
+                        $this->_params['version'],
+                        ldap_errno($conn),
+                        ldap_error($conn),
+                        __FILE__, __LINE__));
+        }
+
+        /* Start TLS if we're using it. */
+        if (!empty($this->_params['tls'])) {
+            if (!@ldap_start_tls($this->_ds)) {
+                Horde::logMessage(
+                    sprintf('STARTTLS failed: [%d] %s',
+                            @ldap_errno($this->_ds),
+                            @ldap_error($this->_ds)),
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        if (isset($this->_params['binddn'])) {
+            $bind = @ldap_bind($this->_ds, $this->_params['binddn'],
+                               $this->_params['password']);
+        } else {
+            $bind = @ldap_bind($this->_ds);
+        }
+
+        if (!$bind) {
+            return PEAR::raiseError(_("Could not bind to LDAP server"));
+        }
+
+        return true;
+    }
+
+    /**
+     * Recursively deletes $dn. $this->_ds MUST already be connected.
+     *
+     * @return mixed  True if delete was successful, PEAR_Error otherwise.
+     */
+    function _recursive_delete($dn)
+    {
+        $search = @ldap_list($this->_ds, $dn, 'objectclass=*', array(''));
+        if (!$search) {
+            return PEAR::raiseError(_("Could not reach the LDAP server"));
+        }
+
+        $children = @ldap_get_entries($this->_ds, $search);
+        for ($i = 0; $i < $children['count']; $i++) {
+            $result = $this->_recursive_delete($children[$i]['dn']);
+            if (!$result) {
+                return PEAR::raiseError(sprintf(_("Group_ldap: Unable to delete group \"%s\". This is what the server said: %s"), $this->getName($children[$i]['dn']), @ldap_error($this->_ds)));
+            }
+        }
+
+        $result = @ldap_delete($this->_ds, $dn);
+        if (!$result) {
+            return PEAR::raiseError(sprintf(_("Group_ldap: Unable to delete group \"%s\". This is what the server said: %s"), $dn, @ldap_error($this->_ds)));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Searches existing groups for the highest gidnumber, and returns
+     * one higher.
+     */
+    function _nextGid()
+    {
+        /* Connect to the LDAP server. */
+        $success = $this->_connect();
+        if (is_a($success, 'PEAR_Error')) {
+            return PEAR::raiseError($success->getMessage());
+        }
+
+        $search = @ldap_search($this->_ds, $this->_params['basedn'], $this->_filter);
+        if (!$search) {
+            return PEAR::raiseError(_("Could not reach the LDAP server"));
+        }
+
+        $result = @ldap_get_entries($this->_ds, $search);
+        @ldap_close($this->_ds);
+
+        if (!is_array($result) || (count($result) <= 1)) {
+            return 1;
+        }
+
+        $nextgid = 0;
+        for ($i = 0; $i < $result['count']; $i++) {
+            if ($result[$i]['gidnumber'][0] > $nextgid) {
+                $nextgid = $result[$i]['gidnumber'][0];
+            }
+        }
+
+        return $nextgid + 1;
+    }
+
+    /**
+     * Return a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's ID (DN)
+     *
+     * @return LDAP_Group  A new group object.
+     * @throws Horde_Exception
+     */
+    function &newGroup($name, $parent = null)
+    {
+        if (empty($name)) {
+            return PEAR::raiseError(_("Group names must be non-empty"));
+        }
+
+        try {
+            $entry = Horde::callHook('groupldap', array($name, $parent));
+        } catch (Horde_Exception_HookNotSet $e) {
+            // Try this simple default and hope it works.
+            $entry[$this->_params['gid']] = $name;
+            $entry['objectclass'] = $this->_params['newgroup_objectclass'];
+            $entry['gidnumber'] = $this->_nextGid();
+        }
+
+        $group = new LDAP_Group($name, $parent);
+        $group->_fromAttributes($entry);
+        $group->setGroupOb($this);
+        return $group;
+    }
+
+    /**
+     * Return an LDAP_Group object corresponding to the named group, with the
+     * users and other data retrieved appropriately.
+     *
+     * @param string $name  The name of the group to retrieve.
+     *
+     * @return LDAP_Group  The requested group.
+     */
+    function &getGroup($name)
+    {
+        $dn = $this->getGroupId($name);
+        if (is_a($dn, 'PEAR_Error')) {
+            return PEAR::raiseError($dn->getMessage());
+        }
+        $group = &$this->getGroupById($dn);
+        return $group;
+    }
+
+    /**
+     * Return an LDAP_Object object corresponding to the given dn, with the
+     * users and other data retrieved appropriately.
+     *
+     * @param string $dn  The dn of the group to retrieve.
+     *
+     * @return LDAP_Object  The requested group.
+     */
+    function &getGroupById($dn)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$dn])) {
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $search = @ldap_search($this->_ds, $dn, $this->_filter);
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return PEAR::raiseError(_("Empty result"));
+            }
+
+            $attributes = array();
+            for ($i = 0; $i < $result[0]['count']; $i++) {
+                if ($result[0][$result[0][$i]]['count'] > 1) {
+                    $attributes[$result[0][$i]] = array();
+                    for ($j = 0; $j < $result[0][$result[0][$i]]['count']; $j++) {
+                        $attributes[$result[0][$i]][] = $result[0][$result[0][$i]][$j];
+                    }
+                } else {
+                    $attributes[$result[0][$i]] = $result[0][$result[0][$i]][0];
+                }
+            }
+            $attributes['dn'] = $result[0]['dn'];
+
+            $group = new LDAP_Group($this->getGroupName($dn));
+            $group->_fromAttributes($attributes);
+            $group->setGroupOb($this);
+            $cache[$dn] = $group;
+        }
+
+        return $cache[$dn];
+    }
+
+    /**
+     * Get a globally unique ID for a group.  This really just returns the dn
+     * for the group, but is included for compatibility with the Group class.
+     *
+     * @param LDAP_Object $group  The group.
+     *
+     * @return string  a GUID referring to $group.
+     */
+    function getGUID($group)
+    {
+        return $group->get('dn');
+    }
+
+    /**
+     * Add a group to the groups system.  The group must first be created with
+     * Group_ldap::newGroup(), and have any initial users added to it, before
+     * this function is called.
+     *
+     * @param LDAP_Group $group  The new group object.
+     *
+     * @return mixed  True if successful, PEAR_Error otherwise.
+     */
+    function addGroup($group)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+
+        /* Connect to the LDAP server. */
+        $success = $this->_connect();
+        if (is_a($success, 'PEAR_Error')) {
+            return PEAR::raiseError($success->getMessage());
+        }
+
+        $dn = $group->get('dn');
+
+        $entry = $group->_toAttributes();
+        $success = @ldap_add($this->_ds, $dn, $entry);
+
+        if (!$success) {
+            return PEAR::raiseError(sprintf(_("Group_ldap: Unable to add group \"%s\". This is what the server said: "), $group->getName()) . @ldap_error($this->_ds));
+        }
+
+        @ldap_close($this->_ds);
+
+        return true;
+    }
+
+    /**
+     * Store updated data - users, etc. - of a group to the backend system.
+     *
+     * @param LDAP_Object $group  The group to update
+     *
+     * @return mixed  True on success, PEAR_Error otherwise.
+     */
+    function updateGroup($group)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+
+        $entry = $group->_toAttributes();
+
+        /* Connect to the LDAP server. */
+        $success = $this->_connect();
+        if (is_a($success, 'PEAR_Error')) {
+            return PEAR::raiseError($success->getMessage());
+        }
+
+        // Do not attempt to change an LDAP object's objectClasses
+        unset($entry['objectclass']);
+
+        $result = @ldap_modify($this->_ds, $group->getId(), $entry);
+        if (!$result) {
+            return PEAR::raiseError(sprintf(_("Group_ldap: Unable to update group \"%s\". This is what the server said: %s"), $group->getName(), @ldap_error($this->_ds)));
+        }
+
+        @ldap_close($this->_ds);
+
+        /* Log the update of the group users on the history log. */
+        $history = &Horde_History::singleton();
+        $guid = $this->getGUID($group);
+        foreach ($group->getAuditLog() as $userId => $action) {
+            $history->log($guid, array('action' => $action, 'user' => $userId), true);
+        }
+        $group->clearAuditLog();
+
+        /* Log the group modification. */
+        $history->log($guid, array('action' => 'modify'), true);
+        return $result;
+    }
+
+    /**
+     * Remove a group from the groups system permanently.
+     *
+     * @param LDAP_Group $group  The group to remove.
+     * @param boolean $force     Recursively delete children groups if true.
+     *
+     * @return mixed  True on success, PEAR_Error otherwise.
+     */
+    function removeGroup($group, $force = false)
+    {
+        if (!is_a($group, 'DataTreeObject_Group')) {
+            return PEAR::raiseError('Groups must be DataTreeObject_Group objects or extend that class.');
+        }
+
+        $dn = $group->getId();
+
+        /* Connect to the LDAP server. */
+        $success = $this->_connect();
+        if (is_a($success, 'PEAR_Error')) {
+            return PEAR::raiseError($success->getMessage());
+        }
+
+        if ($force) {
+            return $this->_recursive_delete($dn);
+        } else {
+            $result = @ldap_delete($this->_ds, $dn);
+            if (!$result) {
+                return PEAR::raiseError(sprintf(_("Group_ldap: Unable to delete group \"%s\". This is what the server said: %s"), $dn, @ldap_error($this->_ds)));
+            }
+        }
+    }
+
+    /**
+     * Retrieve the name of a group.
+     *
+     * @param string $dn  The dn of the group to retrieve the name for.
+     *
+     * @return string  The group's name.
+     */
+    function getGroupName($dn)
+    {
+        $dn = Horde_String::convertCharset($dn, Horde_Nls::getCharset(), 'UTF-8');
+        $result = @ldap_explode_dn($dn, 1);
+        if ($result === false) {
+            return PEAR::raiseError(_("Invalid group ID passed (bad DN syntax)"));
+        }
+
+        return $result[0];
+    }
+
+    /**
+     * DataTreeObject full names include references to parents, but LDAP does
+     * not have this concept.  This function simply returns the $group
+     * parameter and is included for compatibility with the Group class.
+     *
+     * @param string $group  Group name.
+     *
+     * @return string  $group.
+     */
+    function getGroupShortName($group)
+    {
+        return $group;
+    }
+
+    /**
+     * Retrieve the ID of the given group.
+     *
+     * NOTE: If given a group name, this function can be unreliable if more
+     * than one group exists with the same name.
+     *
+     * @param mixed $group   LDAP_Group object, or a group name (string)
+     *
+     * @return string  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        static $cache = array();
+
+        if (is_a($group, 'LDAP_Group')) {
+            return $group->get('dn');
+        }
+
+        if (!isset($cache[$group])) {
+            $this->_connect();
+            $search = @ldap_search($this->_ds, $this->_params['basedn'],
+                                   $this->_params['gid'] . '=' . $group,
+                                   array($this->_params['gid']));
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return PEAR::raiseError(_("Empty result"));
+            }
+            $cache[$group] = $result[0]['dn'];
+        }
+
+        return $cache[$group];
+    }
+
+    /**
+     * Check if a group exists in the system.
+     *
+     * @param string $group  The group name to check for.
+     *
+     * @return boolean  True if the group exists, False otherwise.
+     */
+    function exists($group)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$group])) {
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $groupDN = $this->getGroupId($group);
+            $group = $this->getGroupShortName($group);
+
+            $res = @ldap_compare($this->_ds, $groupDN, $this->_params['gid'], $group);
+            if ($res === false) {
+                return PEAR::raiseError(sprintf(_("Internal Error: An attribute must ALWAYS match itself: %s"), @ldap_error($this->_ds)));
+            }
+            // $res is True if the group exists, -1 if not, false never
+            $cache[$group] = ($res === true);
+        }
+
+        return $cache[$group];
+    }
+
+    /**
+     * Get a list of the parents of a child group.
+     *
+     * @param string $dn  The fully qualified group dn
+     *
+     * @return array  Nested array of parents
+     */
+    function getGroupParents($dn)
+    {
+        $parent = $this->getGroupParent($dn);
+        $parents = array(DATATREE_ROOT => 1);
+        while ($parent != DATATREE_ROOT) {
+            $parents = array($parent => $parents);
+            $parent = $this->getGroupParent($parent);
+        }
+        return $parents;
+    }
+
+    /**
+     * Get the parent of the given group.
+     *
+     * @param string $dn  The dn of the child group.
+     *
+     * @return string  The dn of the parent group.
+     */
+    function getGroupParent($dn)
+    {
+        $result = @ldap_explode_dn($dn, 0);
+        if ($result === false) {
+            return PEAR::raiseError(_("Invalid group ID passed (bad DN syntax)"));
+        }
+
+        unset($result['count']);
+        unset($result[0]);
+        $parent_dn = implode(',', $result);
+
+        if (Horde_String::lower($parent_dn) == Horde_String::lower($GLOBALS['conf']['group']['params']['basedn'])) {
+            return DATATREE_ROOT;
+        } else {
+            return $parent_dn;
+        }
+    }
+
+    /**
+     * Get a list of parents all the way up to the root object for the given
+     * group.
+     *
+     * @param string $dn  The dn of the group.
+     *
+     * @return array  A flat list of all of the parents of the given group,
+     *                hashed in $dn => $name format.
+     */
+    function getGroupParentList($dn)
+    {
+        $result = @ldap_explode_dn($dn, 0);
+        if ($result === false) {
+            return PEAR::raiseError(_("Invalid group ID passed (bad DN syntax)"));
+        }
+
+        $num = $result['count'];
+        unset($result['count']);
+        unset($result[0]);
+
+        $count = 0;
+        $parents = array();
+        $parent_dn = implode(',', $result);
+        while ($parent_dn != $this->_params['basedn'] && $count++ != $num) {
+            $parents[$parent_dn] = $this->getGroupName($parent_dn);
+            unset($result[$count]);
+            $parent_dn = implode(',', $result);
+        }
+        $parents[DATATREE_ROOT] = DATATREE_ROOT;
+
+        return $parents;
+    }
+
+    /**
+     * Get a list of every group, in the format dn => groupname.
+     *
+     * @param boolean $refresh  If true, the cached value is ignored and the
+     *                          group list is refreshed from the group backend.
+     *
+     * @return array  dn => groupname hash.
+     */
+    function listGroups($refresh = false)
+    {
+        static $groups;
+
+        if ($refresh || is_null($groups)) {
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $search = @ldap_search($this->_ds, $this->_params['basedn'], $this->_filter, array($this->_params['gid']));
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            @ldap_sort($this->_ds, $search, $this->_params['gid']);
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return array();
+            }
+
+            $groups = array();
+            for ($i = 0; $i < $result['count']; $i++) {
+                $groups[$result[$i]['dn']] = $this->getGroupName($result[$i]['dn']);
+            }
+        }
+
+        return $groups;
+    }
+
+    /**
+     * Get a list of every user that is part of the specified group and any
+     * of its subgroups.
+     *
+     * @param string $dn  The dn of the parent group.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers($dn)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$dn])) {
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            $search = @ldap_search($this->_ds, $dn, $this->_filter);
+            if (!$search) {
+                return PEAR::raiseError(sprintf(_("Could not reach the LDAP server: %s"), @ldap_error($this->_ds)));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                // Not an error, we just don't have any users in this group.
+                return array();
+            }
+
+            $users = array();
+            for ($i = 0; $i < $result['count']; $i++) {
+                $users = array_merge($users, $this->listUsers($result[$i]['dn']));
+            }
+
+            $cache[$dn] = array_keys(array_flip($users));
+        }
+
+        return $cache[$dn];
+    }
+
+    /**
+     * Get a list of every group that the given user is a member of.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        static $cache = array();
+
+        if (empty($cache[$user])) {
+            /* Connect to the LDAP server. */
+            $success = $this->_connect();
+            if (is_a($success, 'PEAR_Error')) {
+                return PEAR::raiseError($success->getMessage());
+            }
+
+            // Set up search filter
+            $filter = '(' . $this->_params['memberuid'] . '=';
+            if ($GLOBALS['conf']['group']['params']['attrisdn']) {
+                $filter .= $GLOBALS['conf']['auth']['params']['uid'] . '=';
+            }
+            $filter .= $user;
+            if ($GLOBALS['conf']['group']['params']['attrisdn']) {
+                $filter .= ',' . $GLOBALS['conf']['auth']['params']['basedn'];
+            }
+            $filter .= ')';
+
+            // Perform search
+            $search = @ldap_search($this->_ds, $this->_params['basedn'], $filter);
+            if (!$search) {
+                return PEAR::raiseError(_("Could not reach the LDAP server"));
+            }
+
+            $result = @ldap_get_entries($this->_ds, $search);
+            @ldap_close($this->_ds);
+            if (!is_array($result) || (count($result) <= 1)) {
+                return array();
+            }
+
+            $groups = array();
+            $current_charset = Horde_Nls::getCharset();
+            for ($i = 0; $i < $result['count']; $i++) {
+                $utf8_dn = Horde_String::convertCharset($result[$i]['dn'], 'UTF-8', $current_charset);
+                $groups[$utf8_dn] = $this->getGroupName($utf8_dn);
+            }
+
+            $cache[$user] = $groups;
+        }
+
+        return $cache[$user];
+    }
+
+    /**
+     * Returns the tree depth of the given group, relative to the base dn.
+     * 0 is returned for any object directly below the base dn.
+     *
+     * @param string $dn  The dn of the object.
+     *
+     * @return intenger  The tree depth of the group.
+     */
+    function getLevel($dn)
+    {
+        $base = @ldap_explode_dn($this->_params['basedn'], 0);
+        if ($base === false) {
+            return PEAR::raiseError(_("Invalid basedn configured"));
+        }
+
+        $group = @ldap_explode_dn($dn, 0);
+        if ($group === false) {
+            return PEAR::raiseError(_("Invalid group ID passed (bad DN syntax)"));
+        }
+
+        return $group['count'] - $base['count'] - 1;
+    }
+
+}
+
+/**
+ * Extension of the DataTreeObject_Group class for storing group information
+ * in an LDAP directory.
+ *
+ * @author  Ben Chavet <ben@horde.org>
+ * @since   Horde 3.1
+ * @package Horde_Group
+ */
+class LDAP_Group extends DataTreeObject_Group {
+
+    /**
+     * Constructor.
+     *
+     * @param string $name    The name of this group.
+     * @param string $parent  The dn of the parent of this group.
+     */
+    function LDAP_Group($name, $parent = null)
+    {
+        parent::DataTreeObject_Group($name);
+        if ($parent) {
+            $this->data['dn'] = Horde_String::lower($GLOBALS['conf']['group']['params']['gid']) . '=' . $name . ',' . $parent;
+        } else {
+            $this->data['dn'] = Horde_String::lower($GLOBALS['conf']['group']['params']['gid']) . '=' . $name .
+                ',' . Horde_String::lower($GLOBALS['conf']['group']['params']['basedn']);
+        }
+    }
+
+    /**
+     * Get a list of every user that is part of this group (and only
+     * this group).
+     *
+     * @return array  The user list.
+     */
+    function listUsers()
+    {
+        return $this->_groupOb->listUsers($this->data['dn']);
+    }
+
+    /**
+     * Get a list of every user that is a member of this group and any of
+     * it's subgroups.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers()
+    {
+        return $this->_groupOb->listAllUsers($this->data['dn']);
+    }
+
+    /**
+     * Take in a list of attributes from the backend and map it to our
+     * internal data array.
+     *
+     * @param array $attributes  The list of attributes from the backend.
+     */
+    function _fromAttributes($attributes = array())
+    {
+        $this->data['users'] = array();
+        foreach ($attributes as $key => $value) {
+            if (Horde_String::lower($key) == Horde_String::lower($GLOBALS['conf']['group']['params']['memberuid'])) {
+                if (is_array($value)) {
+                    foreach ($value as $user) {
+                        if ($GLOBALS['conf']['group']['params']['attrisdn']) {
+                            $pattern = '/^' . $GLOBALS['conf']['auth']['params']['uid'] . '=([^,]+).*$/';
+                            $results = array();
+                            preg_match($pattern, $user, $results);
+                            if (isset($results[1])) {
+                                $user = $results[1];
+                            }
+                        }
+                        $this->data['users'][$user] = '1';
+                    }
+                } else {
+                    if ($GLOBALS['conf']['group']['params']['attrisdn']) {
+                        $pattern = '/^' . $GLOBALS['conf']['auth']['params']['uid'] . '=([^,]+).*$/';
+                        $results = array();
+                        preg_match($pattern, $value, $results);
+                        if (isset($results[1])) {
+                            $value = $results[1];
+                        }
+                    }
+                    $this->data['users'][$value] = '1';
+                }
+            } elseif ($key == 'mail') {
+                $this->data['email'] = $value;
+            } else {
+                $this->data[$key] = $value;
+            }
+        }
+    }
+
+    /**
+     * Map this object's attributes from the data array into a format that
+     * can be stored in an LDAP entry.
+     *
+     * @return array  The entry array.
+     */
+    function _toAttributes()
+    {
+        $attributes = array();
+        foreach ($this->data as $key => $value) {
+            if ($key == 'users') {
+                foreach ($value as $user => $membership) {
+                    if ($GLOBALS['conf']['group']['params']['attrisdn']) {
+                        $user = $GLOBALS['conf']['auth']['params']['uid'] .
+                            '=' . $user . ',' . $GLOBALS['conf']['auth']['params']['basedn'];
+                    }
+                    $attributes[Horde_String::lower($GLOBALS['conf']['group']['params']['memberuid'])][] = $user;
+                }
+            } elseif ($key == 'email') {
+                if (!empty($value)) {
+                    $attributes['mail'] = $value;
+                }
+            } elseif ($key != 'dn' && $key != Horde_String::lower($GLOBALS['conf']['group']['params']['memberuid'])) {
+                $attributes[$key] = !empty($value) ? $value : ' ';
+            }
+        }
+
+        return $attributes;
+    }
+
+}
diff --git a/framework/Group/Group/mock.php b/framework/Group/Group/mock.php
new file mode 100644 (file)
index 0000000..4084ee8
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+/**
+ * The Group:: class provides the Horde groups system.
+ *
+ * $Horde: framework/Group/Group/mock.php,v 1.3 2009/01/06 17:49:18 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Duck <duck@obala.net>
+ * @since   Horde 3.2
+ * @package Horde_Group
+ */
+class Group_mock extends Group {
+
+    /**
+     * Constructor.
+     */
+    function Group_mock()
+    {
+    }
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+    }
+
+    /**
+     * Returns a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's name.
+     *
+     * @return DataTreeObject_Group  A new group object.
+     */
+    function &newGroup($name, $parent = GROUP_ROOT)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Returns a DataTreeObject_Group object corresponding to the named group,
+     * with the users and other data retrieved appropriately.
+     *
+     * @param string $name The name of the group to retrieve.
+     */
+    function &getGroup($name)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Returns a DataTreeObject_Group object corresponding to the given unique
+     * ID, with the users and other data retrieved appropriately.
+     *
+     * @param integer $cid  The unique ID of the group to retrieve.
+     */
+    function &getGroupById($cid)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Adds a group to the groups system. The group must first be created with
+     * Group::newGroup(), and have any initial users added to it, before this
+     * function is called.
+     *
+     * @param DataTreeObject_Group $group  The new group object.
+     */
+    function addGroup($group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Stores updated data - users, etc. - of a group to the backend system.
+     *
+     * @param DataTreeObject_Group $group  The group to update.
+     */
+    function updateGroup($group)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Removes a group from the groups system permanently.
+     *
+     * @param DataTreeObject_Group $group  The group to remove.
+     * @param boolean $force               Force to remove every child.
+     */
+    function removeGroup($group, $force = false)
+    {
+        return PEAR::raiseError(_("Unsupported"));
+    }
+
+    /**
+     * Retrieves the name of a group.
+     *
+     * @param integer|DataTreeObject_Group $gid  The id of the group or the
+     *                                           group object to retrieve the
+     *                                           name for.
+     *
+     * @return string  The group's name.
+     */
+    function getGroupName($gid)
+    {
+        return '';
+    }
+
+    /**
+     * Strips all parent references off of the given group name.
+     *
+     * @param string $group  Name of the group.
+     *
+     * @return The name of the group without parents.
+     */
+    function getGroupShortName($group)
+    {
+        return '';
+    }
+
+    /**
+     * Retrieves the ID of a group.
+     *
+     * @param string|DataTreeObject_Group $group  The group name or object to
+     *                                            retrieve the ID for.
+     *
+     * @return integer  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        return '';
+    }
+
+    /**
+     * Check if a group exists in the system.
+     *
+     * @param string $group  The group to check.
+     *
+     * @return boolean  True if the group exists, false otherwise.
+     */
+    function exists($group)
+    {
+        return false;
+    }
+
+    /**
+     * Returns a tree of the parents of a child group.
+     *
+     * @param integer $gid  The id of the child group.
+     *
+     * @return array  The group parents tree, with groupnames as the keys.
+     */
+    function getGroupParents($gid)
+    {
+        return array();
+    }
+
+    /**
+     * Returns the single parent ID of the given group.
+     *
+     * @param integer $gid  The DataTree ID of the child group.
+     *
+     * @return integer  The parent of the given group.
+     */
+    function getGroupParent($gid)
+    {
+        return null;
+    }
+
+    /**
+     * Returns a flat list of the parents of a child group
+     *
+     * @param integer $gid  The id of the group.
+     *
+     * @return array  A flat list of all of the parents of $group, hashed in
+     *                $id => $name format.
+     */
+    function getGroupParentList($gid)
+    {
+        return array();
+    }
+
+    /**
+     * Returns a list of all groups, in the format id => groupname.
+     *
+     * @param boolean $refresh  If true, the cached value is ignored and the
+     *                          group list is refreshed from the group backend.
+     *
+     * @return array  ID => groupname hash.
+     */
+    function listGroups($refresh = false)
+    {
+        return array();
+    }
+
+    /**
+     * Get a list of every user that is a part of this group ONLY.
+     *
+     * @param integer $gid  The ID of the group.
+     *
+     * @return array  The user list.
+     */
+    function listUsers($gid)
+    {
+        return array();
+    }
+
+    /**
+     * Get a list of every user that is part of the specified group
+     * and any of its subgroups.
+     *
+     * @param integer $group  The ID of the parent group.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers($gid)
+    {
+        return array();
+    }
+
+    /**
+     * Get a list of every group that $user is in.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        return array();
+    }
+
+    /**
+     * Say if a user is a member of a group or not.
+     *
+     * @param string $user        The name of the user.
+     * @param integer $gid        The ID of the group.
+     * @param boolean $subgroups  Return true if the user is in any subgroups
+     *                            of group with ID $gid, also.
+     *
+     * @return boolean
+     */
+    function userIsInGroup($user, $gid, $subgroups = true)
+    {
+        return false;
+    }
+
+    /**
+     * Returns the nesting level of the given group. 0 is returned for any
+     * object directly below GROUP_ROOT.
+     *
+     * @param integer $gid  The ID of the group.
+     *
+     * @return The nesting level of the group.
+     */
+    function getLevel($gid)
+    {
+        return 0;
+    }
+
+    /**
+     * Stores the object in the session cache.
+     */
+    function shutdown()
+    {
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+    }
+
+}
diff --git a/framework/Group/Group/sql.php b/framework/Group/Group/sql.php
new file mode 100644 (file)
index 0000000..6a7fb15
--- /dev/null
@@ -0,0 +1,891 @@
+<?php
+/**
+ * The Group:: class provides the Horde groups system.
+ *
+ * $Horde: framework/Group/Group/sql.php,v 1.14 2009/07/17 21:00:12 slusarz Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Duck <duck@obala.net>
+ * @since   Horde 3.2
+ * @package Horde_Group
+ */
+class Group_sql extends Group {
+
+    /**
+     * Boolean indicating whether or not we're connected to the SQL server.
+     *
+     * @var boolean
+     */
+    var $_connected = false;
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * Constructor.
+     */
+    function Group_sql($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+    }
+
+    /**
+     * Stores the object in the session cache.
+     */
+    function shutdown()
+    {
+    }
+
+    /**
+     * Replace all occurences of ':' in an object name with '.'.
+     *
+     * @param string $name  The name of the object.
+     *
+     * @return string  The encoded name.
+     */
+    function encodeName($name)
+    {
+        return str_replace(':', '.', $name);
+    }
+
+    /**
+     * Returns a new group object.
+     *
+     * @param string $name    The group's name.
+     * @param string $parent  The group's parent's name.
+     *
+     * @return SQLObject_Group  A new group object.
+     */
+    function &newGroup($name, $parent = GROUP_ROOT)
+    {
+        if (empty($name)) {
+            return PEAR::raiseError(_("Group names must be non-empty"));
+        }
+
+        if ($parent != GROUP_ROOT) {
+            $name = $this->getGroupName($parent) . ':' . $this->encodeName($name);
+        }
+
+        $group = new SQLObject_Group($name);
+        $group->setGroupOb($this);
+        return $group;
+    }
+
+    /**
+     * Returns a SQLObject_Group object corresponding to the named group,
+     * with the users and other data retrieved appropriately.
+     *
+     * @param string $name The name of the group to retrieve.
+     */
+    function &getGroup($name)
+    {
+        if (!isset($this->_groupCache[$name])) {
+            $this->_connect();
+            $sql = 'SELECT group_uid, group_email FROM horde_groups WHERE group_name = ?';
+            $group = $this->_db->getRow($sql, array($name), DB_FETCHMODE_ASSOC);
+
+            if (is_a($group, 'PEAR_Error')) {
+                return $group;
+            } elseif (empty($group)) {
+                return PEAR::raiseError($name . ' does not exist');
+            }
+
+            $sql = 'SELECT user_uid FROM horde_groups_members '
+                . ' WHERE group_uid = ? ORDER BY user_uid ASC';
+            $users = $this->_db->getCol($sql, 0, array($group['group_uid']));
+            if (is_a($users, 'PEAR_Error')) {
+                return $users;
+            }
+
+            $object = new SQLObject_Group($name);
+            $object->id = $group['group_uid'];
+            $object->data['email'] = $group['group_email'];
+
+            if (!empty($users)) {
+                $object->data['users'] = array_flip($users);
+            }
+
+            $this->_groupCache[$name] = $object;
+            $this->_groupCache[$name]->setGroupOb($this);
+            $this->_groupMap[$this->_groupCache[$name]->getId()] = $name;
+        }
+
+        return $this->_groupCache[$name];
+    }
+
+    /**
+     * Returns a SQLObject_Group object corresponding to the given unique
+     * ID, with the users and other data retrieved appropriately.
+     *
+     * @param integer $cid  The unique ID of the group to retrieve.
+     */
+    function &getGroupById($cid)
+    {
+        if (isset($this->_groupMap[$cid])) {
+            return $this->_groupCache[$this->_groupMap[$cid]];
+        }
+
+        $this->_connect();
+        $sql = 'SELECT group_name, group_email FROM horde_groups WHERE group_uid = ?';
+        $row = $this->_db->getRow($sql, array($cid), DB_FETCHMODE_ASSOC);
+
+        if (is_a($row, 'PEAR_Error')) {
+            return $row;
+        } elseif (empty($row)) {
+            return PEAR::raiseError($cid . ' does not exist');
+        }
+
+        $sql = 'SELECT user_uid FROM horde_groups_members '
+            . ' WHERE group_uid = ? ORDER BY user_uid ASC';
+        $users = $this->_db->getCol($sql, 0, array($cid));
+        if (is_a($users, 'PEAR_Error')) {
+            return $users;
+        }
+
+        $group = new SQLObject_Group($row['group_name']);
+        $group->id = $cid;
+        $group->data['email'] = $row['group_email'];
+
+        if (!empty($users)) {
+            $group->data['users'] = array_flip($users);
+        }
+
+        $group->setGroupOb($this);
+        $name = $group->getName();
+        $this->_groupCache[$name] = &$group;
+        $this->_groupMap[$cid] = $name;
+
+        return $group;
+    }
+
+    /**
+     * Adds a group to the groups system. The group must first be created with
+     * Group::newGroup(), and have any initial users added to it, before this
+     * function is called.
+     *
+     * @param SQLObject_Group $group  The new group object.
+     */
+    function addGroup(&$group)
+    {
+        if (!is_a($group, 'SQLObject_Group')) {
+            return PEAR::raiseError('Groups must be SQLObject_Group objects or extend that class.');
+        }
+
+        $this->_connect();
+        $group->setGroupOb($this);
+        $name = $group->getName();
+
+        $email = isset($group->data['email']) ? $group->data['email'] : '';
+        $group_id = $this->_write_db->nextId('horde_groups');
+        if (is_a($group_id, 'PEAR_Error')) {
+            return $group_id;
+        }
+
+        $group->id = $group_id;
+        $query = 'INSERT INTO horde_groups (group_uid, group_name, group_parents, group_email) VALUES (?, ?, ?, ?)';
+        $result = $this->_write_db->query($query, array($group->id, $name, '', $email));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!empty($group->data['users'])) {
+            $query = 'INSERT INTO horde_groups_members (group_uid, user_uid)'
+                .' VALUES (' . (int)$group->id . ', ?)';
+            $sth = $this->_write_db->prepare($query);
+            $result = $this->_write_db->executeMultiple($sth, $group->data['users']);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $this->_groupCache[$name] = &$group;
+        $this->_groupMap[$group_id] = $name;
+        if (isset($this->_groupList)) {
+            $this->_groupList[$group_id] = $name;
+        }
+
+        /* Log the addition of the group in the history log. */
+        $history = &Horde_History::singleton();
+        $log = $history->log($this->getGUID($group), array('action' => 'add'), true);
+        if (is_a($log, 'PEAR_Error')) {
+            return $log;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Stores updated data - users, etc. - of a group to the backend system.
+     *
+     * @param SQLObject_Group $group  The group to update.
+     */
+    function updateGroup($group)
+    {
+        if (!is_a($group, 'SQLObject_Group')) {
+            return PEAR::raiseError('Groups must be SQLObject_Group objects or extend that class.');
+        }
+
+        $this->_connect();
+
+        $query = 'UPDATE horde_groups SET group_email = ? WHERE group_uid = ?';
+        $result = $this->_write_db->query($query, array($this->data['email'], $this->id));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'DELETE FROM horde_groups_members WHERE group_uid = ?';
+        $result = $this->_write_db->query($query, array($this->id));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'INSERT INTO horde_groups_members (group_uid, user_uid)'
+            .' VALUES (' . (int)$this->id . ', ?)';
+        $sth = $this->_write_db->prepare($query);
+        $result = $this->_groupOb->_write_db->executeMultiple($sth, $this->data['users']);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_groupCache[$group->getName()] = &$group;
+
+        /* Log the update of the group users on the history log. */
+        $history = &Horde_History::singleton();
+        $guid = $this->getGUID($group);
+        foreach ($group->getAuditLog() as $userId => $action) {
+            $history->log($guid, array('action' => $action, 'user' => $userId), true);
+        }
+
+        $group->clearAuditLog();
+
+        /* Log the group modification. */
+        $history->log($guid, array('action' => 'modify'), true);
+        return $result;
+    }
+
+    /**
+     * Removes a group from the groups system permanently.
+     *
+     * @param SQLObject_Group $group  The group to remove.
+     * @param boolean $force               Force to remove every child.
+     */
+    function removeGroup($group, $force = false)
+    {
+        if (!is_a($group, 'SQLObject_Group')) {
+            return PEAR::raiseError('Groups must be SQLObject_Group objects or extend that class.');
+        }
+
+        $this->_connect();
+        $id = $group->getId();
+        $name = $group->getName();
+        unset($this->_groupMap[$id]);
+        if (isset($this->_groupList)) {
+            unset($this->_groupList[$id]);
+        }
+        unset($this->_groupCache[$name]);
+
+        $history = &Horde_History::singleton();
+        $history->log($this->getGUID($group), array('action' => 'delete'), true);
+
+        $query = 'DELETE FROM horde_groups_members WHERE group_uid = ?';
+        $result = $this->_write_db->query($query, array($id));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'DELETE FROM horde_groups WHERE group_uid = ?';
+        $result = $this->_write_db->query($query, array($id));
+        if (!$force || is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $query = 'DELETE FROM horde_groups WHERE group_name LIKE ?';
+        return $this->_write_db->query($query, array($name . ':%'));
+    }
+
+    /**
+     * Retrieves the name of a group.
+     *
+     * @param integer|SQLObject_Group $gid  The id of the group or the
+     *                                           group object to retrieve the
+     *                                           name for.
+     *
+     * @return string  The group's name.
+     */
+    function getGroupName($gid)
+    {
+        if (is_a($gid, 'SQLObject_Group')) {
+            $gid = $gid->getId();
+        }
+
+        if (isset($this->_groupMap[$gid])) {
+            return $this->_groupMap[$gid];
+        }
+        if (isset($this->_groupList[$gid])) {
+            return $this->_groupList[$gid];
+        }
+
+        $this->_connect();
+        $query = 'SELECT group_name FROM horde_groups WHERE group_uid = ?';
+        return $this->_db->getOne($query, $gid);
+    }
+
+    /**
+     * Strips all parent references off of the given group name.
+     *
+     * @param string $group  Name of the group.
+     *
+     * @return The name of the group without parents.
+     */
+    function getGroupShortName($group)
+    {
+        /* If there are several components to the name, explode and get the
+         * last one, otherwise just return the name. */
+        if (strpos($group, ':') !== false) {
+            $name = explode(':', $group);
+            return array_pop($name);
+        }
+
+        return $group;
+    }
+
+    /**
+     * Retrieves the ID of a group.
+     *
+     * @param string|SQLObject_Group $group  The group name or object to
+     *                                            retrieve the ID for.
+     *
+     * @return integer  The group's ID.
+     */
+    function getGroupId($group)
+    {
+        if (is_a($group, 'SQLObject_Group')) {
+            return $group->getId();
+        }
+
+        $id = array_search($group, $this->_groupMap);
+        if ($id !== false) {
+            return $id;
+        }
+        if (isset($this->_groupList)) {
+            $id = array_search($group, $this->_groupList);
+            if ($id !== false) {
+                return $id;
+            }
+        }
+
+        $this->_connect();
+        $query = 'SELECT group_uid FROM horde_groups WHERE group_name = ?';
+        return $this->_db->getOne($query, $group);
+    }
+
+    /**
+     * Check if a group exists in the system.
+     *
+     * @param string $group  The group to check.
+     *
+     * @return boolean  True if the group exists, false otherwise.
+     */
+    function exists($group)
+    {
+        if (isset($this->_groupCache[$group]) ||
+            (isset($this->_groupList) &&
+             array_search($group, $this->_groupList) !== false)) {
+            return true;
+        }
+
+        $this->_connect();
+        $query = 'SELECT COUNT(*) FROM horde_groups WHERE group_name = ?';
+        return (bool)$this->_db->getOne($query, $group);
+    }
+
+    /**
+     * Returns a tree of the parents of a child group.
+     *
+     * @param integer $gid  The id of the child group.
+     *
+     * @return array  The group parents tree, with groupnames as the keys.
+     */
+    function getGroupParents($gid)
+    {
+        if (!isset($this->_parentTree[$gid])) {
+            $name = $this->getGroupName($gid);
+            $this->_connect();
+            $parents = $this->_getGroupParents($name);
+            if (is_a($parents, 'PEAR_Error')) {
+                return $parents;
+            }
+
+            $this->_parentTree[$gid] = $parents;
+        }
+
+        return $this->_parentTree[$gid];
+    }
+
+    /**
+     * Returns a list of parent permissions.
+     *
+     * @param string $child  The name of the child to retrieve parents for.
+     *
+     * @return array  A hash with all parents in a tree format.
+     */
+    function _getGroupParents($child)
+    {
+        if (($pos = strrpos($child, ':')) !== false) {
+            $child = substr($child, 0, $pos);
+        }
+
+        return $this->_getParents($child);
+    }
+
+    /**
+     */
+    function _getParents($parents)
+    {
+        $mother = array();
+        if (!empty($parents)) {
+            $pname = $parents;
+            $parents = substr($parents, 0, strrpos($parents, ':'));
+            $mother[$pname] = $this->_getParents($parents);
+        } else {
+            return array(GROUP_ROOT => true);
+        }
+
+        return $mother;
+    }
+
+    /**
+     * Returns the single parent ID of the given group.
+     *
+     * @param integer $gid  The ID of the child group.
+     *
+     * @return integer  The parent of the given group.
+     */
+    function getGroupParent($gid)
+    {
+        if (!isset($this->_groupParents[$gid])) {
+            $this->_connect();
+
+            $name = $this->getGroupName($gid);
+            if (is_a($name, 'PEAR_Error')) {
+                return $name;
+            }
+
+            if (($pos = strrpos($name, ':')) !== false) {
+                $this->_groupParents[$gid] = $this->getGroupId(substr($name, 0, $pos));
+            } else {
+                $this->_groupParents[$gid] = GROUP_ROOT;
+            }
+        }
+
+        return $this->_groupParents[$gid];
+    }
+
+    /**
+     * Returns a flat list of the parents of a child group
+     *
+     * @param integer $gid  The id of the group.
+     *
+     * @return array  A flat list of all of the parents of $group, hashed in
+     *                $id => $name format.
+     */
+    function getGroupParentList($gid)
+    {
+        if (!isset($this->_groupParentList[$gid])) {
+            $name = $this->getGroupName($gid);
+            $pos = strpos($name, ':');
+            if ($pos == false) {
+                $this->_groupParentList[$gid] = array();
+                return $this->_groupParentList[$gid];
+            }
+
+            $parents = array();
+            while ($pos) {
+                $name = substr($name, 0, $pos);
+                $parents[] = $name;
+                $pos = strpos($name, ':');
+            }
+
+            $query = 'SELECT group_uid, group_name FROM horde_groups '
+                . ' WHERE group_name IN (' . str_repeat('?, ', count($parents) - 1) . '?) ';
+            $parents = $this->_db->getAssoc($query, false, $parents);
+            if (is_a($parents, 'PEAR_Error')) {
+                return $parents;
+            }
+
+            $this->_groupParentList[$gid] = $parents;
+        }
+
+        return $this->_groupParentList[$gid];
+    }
+
+    /**
+     * Returns a flat list of the parents of a child group
+     *
+     * @param integer $gid  The id of the group.
+     *
+     * @return array  A flat list of all of the parents of $group, hashed in
+     *                $id => $name format.
+     */
+    function _getGroupParentNameList($name)
+    {
+        $parents = array();
+
+        while ($pos) {
+            $name = substr($name, 0, $pos);
+            $parents[] = $name;
+            $pos = strpos($name, ':');
+        }
+
+        return $parents;
+    }
+
+    /**
+     * Returns a list of all groups, in the format id => groupname.
+     *
+     * @param boolean $refresh  If true, the cached value is ignored and the
+     *                          group list is refreshed from the group backend.
+     *
+     * @return array  ID => groupname hash.
+     */
+    function listGroups($refresh = false)
+    {
+        if ($refresh || !isset($this->_groupList)) {
+            $this->_connect();
+            $sql = 'SELECT group_uid, group_name FROM horde_groups ORDER BY group_uid';
+            $this->_groupList = $this->_db->getAssoc($sql);
+        }
+
+        return $this->_groupList;
+    }
+
+    /**
+     * Get a list of every user that is part of the specified group
+     * and any of its subgroups.
+     *
+     * @param integer $group  The ID of the parent group.
+     *
+     * @return array  The complete user list.
+     */
+    function listAllUsers($gid)
+    {
+        if (!isset($this->_subGroups[$gid])) {
+            // Get a list of every group that is a sub-group of $group.
+            $name = $this->getGroupName($gid);
+            $query = 'SELECT group_uid FROM horde_groups WHERE group_name LIKE ?';
+            $parents = $this->_db->getCol($query, 0, array($name .  ':%'));
+            $this->_subGroups[$gid] = $parents;
+            $this->_subGroups[$gid][] = $gid;
+        }
+
+        $users = array();
+        foreach ($this->_subGroups[$gid] as $groupId) {
+            $users = array_merge($users, $this->listUsers($groupId));
+        }
+
+        return array_values(array_flip(array_flip($users)));
+    }
+
+    /**
+     * Get a list of every group that $user is in.
+     *
+     * @param string  $user          The user to get groups for.
+     * @param boolean $parentGroups  Also return the parents of any groups?
+     *
+     * @return array  An array of all groups the user is in.
+     */
+    function getGroupMemberships($user, $parentGroups = false)
+    {
+        if (isset($_SESSION['horde']['groups']['m'][$user][$parentGroups])) {
+            return $_SESSION['horde']['groups']['m'][$user][$parentGroups];
+        }
+
+        $this->_connect();
+
+        $sql = 'SELECT g.group_uid AS group_uid, g.group_name AS group_name FROM horde_groups g, horde_groups_members m '
+            . ' WHERE m.user_uid = ? AND g.group_uid = m.group_uid ORDER BY g.group_name';
+        $result = $this->_db->query($sql, $user);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $groups = array();
+        while ($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
+            $groups[(int)$row['group_uid']] = $this->getGroupShortName($row['group_name']);
+        }
+
+        if ($parentGroups) {
+            foreach ($groups as $id => $g) {
+                $parents = $this->getGroupParentList($id);
+                if (is_a($parents, 'PEAR_Error')) {
+                    return $parents;
+                }
+                $groups += $parents;
+            }
+        }
+
+        $_SESSION['horde']['groups']['m'][$user][$parentGroups] = $groups;
+        return $groups;
+    }
+
+    /**
+     * Say if a user is a member of a group or not.
+     *
+     * @param string $user        The name of the user.
+     * @param integer $gid        The ID of the group.
+     * @param boolean $subgroups  Return true if the user is in any subgroups
+     *                            of group with ID $gid, also.
+     *
+     * @return boolean
+     */
+    function userIsInGroup($user, $gid, $subgroups = true)
+    {
+        if (isset($_SESSION['horde']['groups']['i'][$user][$subgroups][$gid])) {
+            return $_SESSION['horde']['groups']['i'][$user][$subgroups][$gid];
+        }
+
+        if ($subgroups) {
+            $groups = $this->getGroupMemberships($user, true);
+            if (is_a($groups, 'PEAR_Error')) {
+                Horde::logMessage($groups, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return false;
+            }
+
+            $result = !empty($groups[$gid]);
+        } else {
+            $this->_connect();
+            $query = 'SELECT COUNT(*) FROM horde_groups_members WHERE group_uid = ? AND user_uid = ?';
+            $result = $this->_db->getOne($query, array($gid, $user));
+
+        }
+
+        $_SESSION['horde']['groups']['i'][$user][$subgroups][$gid] = (bool)$result;
+        return (bool)$result;
+    }
+
+    /**
+     * Attempts to open a persistent connection to the sql server.
+     *
+     * @return boolean  True on success.
+     * @throws Horde_Exception
+     */
+    function _connect()
+    {
+        if ($this->_connected) {
+            return true;
+        }
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+
+        /* Connect to the sql server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_write_db = DB::connect($this->_params,
+                                       array('persistent' => !empty($this->_params['persistent']),
+                                             'ssl' => !empty($this->_params['ssl'])));
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            throw new Horde_Exception($this->_write_db);
+        }
+
+        /* Set DB portability options. */
+        switch ($this->_write_db->phptype) {
+        case 'mssql':
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+        default:
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+        }
+
+        /* Check if we need to set up the read DB connection seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = DB::connect($params,
+                                     array('persistent' => !empty($params['persistent']),
+                                           'ssl' => !empty($params['ssl'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                throw new Horde_Exception($this->_db);
+            }
+
+            /* Set DB portability options. */
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+        } else {
+            /* Default to the same DB handle for the writer too. */
+            $this->_db = $this->_write_db;
+        }
+
+        $this->_connected = true;
+        return true;
+    }
+
+}
+
+/**
+ * Extension of the SQLObject class for storing Group information
+ * in the Categories driver. If you want to store specialized Group
+ * information, you should extend this class instead of extending
+ * SQLObject directly.
+ *
+ * @author  Duck <duck@obala.net>
+ * @since   Horde 3.2
+ * @package Horde_Group
+ */
+class SQLObject_Group extends DataTreeObject_Group {
+
+    /**
+     * The unique name of this object.
+     * These names have the same requirements as other object names - they must
+     * be unique, etc.
+     *
+     * @var string
+     */
+    var $name;
+
+    /**
+     * The unique name of this object.
+     * These names have the same requirements as other object names - they must
+     * be unique, etc.
+     *
+     * @var integer
+     */
+    var $id;
+
+    /**
+     * Key-value hash that will be serialized.
+     *
+     * @see getData()
+     * @var array
+     */
+    var $data = array();
+
+    /**
+     * The SQLObject_Group constructor. Just makes sure to call
+     * the parent constructor so that the group's name is set
+     * properly.
+     *
+     * @param string $name  The name of the group.
+     */
+    function SQLObject_Group($name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Gets the ID of this object.
+     *
+     * @return string  The object's ID.
+     */
+    function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Gets the name of this object.
+     *
+     * @return string The object name.
+     */
+    function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Gets one of the attributes of the object, or null if it isn't defined.
+     *
+     * @param string $attribute  The attribute to get.
+     *
+     * @return mixed  The value of the attribute, or null.
+     */
+    function get($attribute)
+    {
+        return isset($this->data[$attribute])
+            ? $this->data[$attribute]
+            : null;
+    }
+
+    /**
+     * Sets one of the attributes of the object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     */
+    function set($attribute, $value)
+    {
+        $this->data[$attribute] = $value;
+    }
+
+    /**
+     * Save group
+     */
+    function save()
+    {
+        if (isset($this->data['email'])) {
+            $query = 'UPDATE horde_groups SET group_email = ? WHERE group_uid = ?';
+            $result = $this->_groupOb->_write_db->query($query, array($this->data['email'], $this->id));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $query = 'DELETE FROM horde_groups_members WHERE group_uid = ?';
+        $result = $this->_groupOb->_write_db->query($query, array($this->id));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!empty($this->data['users'])) {
+            $query = 'INSERT INTO horde_groups_members (group_uid, user_uid)'
+                .' VALUES (' . $this->_groupOb->_write_db->quote($this->id) . ', ?)';
+            $sth = $this->_groupOb->_write_db->prepare($query);
+            $result = $this->_groupOb->_write_db->executeMultiple($sth, array_keys($this->data['users']));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+    }
+
+}
diff --git a/framework/Group/package.xml b/framework/Group/package.xml
new file mode 100644 (file)
index 0000000..37f50da
--- /dev/null
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Group</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde User Groups System</summary>
+ <description>Package for managing and accessing the Horde groups system.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-09-16</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Added a mock driver for installations that don't need groups (Request #6157).
+* Added a beta SQL Group driver (Request #6175).
+* Removed unused renameGroup() function.
+* Fixed loading subclasses before unserializing session objects (Bug #4650)
+* Added caching.
+* Fixed getGroupParents().
+* Fixed listAllUsers().
+* Switched from hook functions for every group to a single hook function for all groups (Request #4324).
+* Added a Group driver for the Kolab groupware server.
+* Allow group members to be stored as DNs in LDAP driver (Bug #4131).
+* Significant changes to the LDAP Group driver (Bug #4135).
+* Fixed chicken and egg problem for creating the first LDAP group (Bug #4668).
+* Fixed nextgid calculation in the LDAP driver (Bug #4699).
+* UTF-8-encoded DNs in the LDAP Groups driver (Bugs #4692 and #4918).
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="Group">
+    <file baseinstalldir="/Horde" name="hooks.php" role="php" />
+    <file baseinstalldir="/Horde" name="kolab.php" role="php" />
+    <file baseinstalldir="/Horde" name="ldap.php" role="php" />
+    <file baseinstalldir="/Horde" name="mock.php" role="php" />
+    <file baseinstalldir="/Horde" name="sql.php" role="php" />
+    <file baseinstalldir="/Horde" name="contactlists.php" role="php" />
+   </dir> <!-- /Group -->
+   <file baseinstalldir="/Horde" name="Group.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.4</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_DataTree</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Auth</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2006-05-08</date>
+   <time>21:51:02</time>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Added ldap driver.
+* Converted to package.xml 2.0 for pear.horde.org.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-05</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Kolab/Kolab.php b/framework/Kolab/Kolab.php
new file mode 100644 (file)
index 0000000..922c02c
--- /dev/null
@@ -0,0 +1,815 @@
+<?php
+
+/** We require the base Horde library. */
+require_once 'Horde.php';
+
+/** We need access to the Kolab IMAP storage */
+require_once 'Horde/Kolab/Deprecated.php';
+
+/** We need the Kolab date functions */
+require_once 'Horde/Kolab/Format/Date.php';
+
+/**
+ * The Horde_Kolab library is both an object used by application drivers to
+ * communicate with a Kolab server, as well as a utility library providing
+ * several functions to help in the IMAP folder <-> Horde Share synchronisation
+ * process.
+ *
+ * $Horde: framework/Kolab/Kolab.php,v 1.89 2009/07/14 00:25:29 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stuart Binge <omicron@mighty.co.za>
+ * @package Horde_Kolab
+ */
+class Kolab {
+
+    /**
+     * The current application that this Kolab object instance is catering to.
+     *
+     * @deprecated
+     *
+     * @var string
+     */
+    var $_app;
+
+    /**
+     * The storage driver for the Kolab server.
+     *
+     * @deprecated
+     *
+     * @var Kolab_Storage
+     */
+    var $_storage;
+
+    /**
+     * Indicates the version of this driver
+     *
+     * @deprecated
+     *
+     * @var int
+     */
+    var $version = 2;
+
+    /**
+     * The DomDocument object that contains the XML DOM tree of the currently
+     * loaded groupware object. We cache this here to ensure preservation of
+     * unknown fields when re-saving the object.
+     *
+     * @deprecated
+     *
+     * @var DomDocument
+     */
+    var $_xml;
+
+    /**
+     * The (Kolab) UID of the current message.
+     *
+     * @deprecated
+     *
+     * @var string
+     */
+    var $_uid;
+
+    /**
+     *
+     * @deprecated
+     *
+     */
+    function Kolab($app = null)
+    {
+        if (!isset($app)) {
+            global $registry;
+            $app = $registry->getApp();
+        }
+        $this->_app = $app;
+
+        $this->_storage = new Kolab_Storage_Deprecated();
+    }
+
+    /**
+     * Return the uid of the message we are currently dealing with.
+     *
+     * @deprecated
+     *
+     * @return string  The Kolab UID of the message we are currently
+     *                 dealing with.
+     */
+    function getUID()
+    {
+      return $this->_uid;
+    }
+
+    /**
+     * Open the specified share.
+     *
+     * @deprecated
+     *
+     * @param string $share_uid      The uid of the share that
+     *                               should be opened.
+     * @param int    $loader         The version of the XML
+     *                               loader
+     *
+     * @return mixed  True on success, a PEAR error otherwise
+     */
+    function open($share_uid, $loader = 0)
+    {
+        $app_consts = Kolab::getAppConsts($this->_app);
+        if (is_a($app_consts, 'PEAR_Error')) {
+            return $app_consts;
+        }
+
+        return $this->_storage->open($share_uid, $app_consts, $loader);
+    }
+
+    /**
+     * Close the current share.
+     *
+     * @deprecated
+     */
+    function close()
+    {
+        $this->_storage->close();
+    }
+
+    /**
+     * Retrieve all objects in the current folder
+     *
+     * @deprecated
+     *
+     * @return array  All object data arrays
+     */
+    function getObjects()
+    {
+        return $this->_storage->getObjects();
+    }
+
+    /**
+     * Returns a list of all IMAP folders (including their groupware type)
+     * that the current user has acccess to.
+     *
+     * @deprecated
+     *
+     * @return array  An array of array($foldername, $foldertype) items (empty
+     *                on error).
+     */
+    function listFolders()
+    {
+        return Kolab_Storage_Deprecated::listFolders();
+    }
+
+    /**
+     * List the objects in the current share.
+     *
+     * @deprecated
+     *
+     * @return mixed  false if there are no objects, a list of message
+     *                ids or a PEAR error.
+     */
+    function listObjects()
+    {
+        return $this->_storage->listObjects();
+    }
+
+    /**
+     * List the objects in the specified folder.
+     *
+     * @deprecated
+     *
+     * @param string $folder  The folder to search.
+     *
+     * @return mixed  false if there are no objects, a list of message
+     *                ids otherwise.
+     */
+    function listObjectsInFolder($folder)
+    {
+        return $this->_storage->listObjectsInFolder($folder);
+    }
+
+    /**
+     * Find the object with the given UID in the current share.
+     *
+     * @deprecated
+     *
+     * @param string $uid  The UID of the object.
+     *
+     * @return mixed  false if there is no such object
+     */
+    function findObject($uid)
+    {
+        return $this->_storage->findObject($uid);
+    }
+
+    /**
+     * Load the object with the given UID into $this->_xml
+     *
+     * @deprecated
+     *
+     * @param string  $uid      The UID of the object.
+     * @param boolean $is_msgno Indicate if $uid holds an
+     *                          IMAP message number
+     *
+     * @return mixed  false if there is no such object, a PEAR error if
+     *                the object could not be loaded. Otherwise the xml
+     *                document will be returned
+     */
+    function &loadObject($uid, $is_msgno = false)
+    {
+        $result = $this->_storage->loadObject($uid, $is_msgno);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_xml = $result;
+        $this->_uid = $this->getVal('uid');
+        $element = $this->_xml->document_element();
+        return $element;
+
+    }
+
+    /**
+     * Create the object with UID in the current share
+     *
+     * @deprecated
+     *
+     * @param string  $uid      The UID of the object.
+     *
+     * @return mixed  false if there is no open share, a PEAR error if
+     *                the object could not be created. Otherwise the xml
+     *                document will be returned
+     */
+    function &newObject($uid)
+    {
+        $result = $this->_storage->newObject($uid);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_xml = $result;
+        $this->_uid = $uid;
+
+        $element = $this->_xml->document_element();
+        return $element;
+    }
+
+    /**
+     * Save the current object.
+     *
+     * @deprecated
+     *
+     * @return mixed  false if there is no open share, a PEAR error if
+     *                the object could not be saved. True otherwise
+     */
+    function saveObject()
+    {
+        $this->setVal('last-modification-date', Kolab::encodeDateTime());
+        $this->setVal('product-id', KOLAB_PRODUCT_ID);
+
+        return $this->_storage->saveObject($this->_xml, $this->_uid);
+    }
+
+    /**
+     * Move the object with the given UID from the current share into
+     * the specified new share.
+     *
+     * @deprecated
+     *
+     * @param string  $uid       The UID of the object.
+     * @param boolean $new_share The share the object should be moved to.
+     *
+     * @return mixed  false if there is no current share, a PEAR error if
+     *                the object could not be moved. True otherwise.
+     */
+    function moveObject($uid, $new_share)
+    {
+        return $this->_storage->moveObject($uid, $new_share);
+    }
+
+    /**
+     * Remove the specified objects from the current share.
+     *
+     * @deprecated
+     *
+     * @param string  $objects  The UIDs (or maessage numbers)
+     *                          of the objects to be deleted.
+     * @param boolean $is_msgno Indicate if $objects holds
+     *                          IMAP message numbers
+     *
+     * @return mixed  false if there is no IMAP connection, a PEAR
+     *                error if the objects could not be removed. True
+     *                if the call succeeded.
+     */
+    function removeObjects($objects, $is_msgno = false)
+    {
+        return $this->_storage->removeObjects($objects, $is_msgno);
+    }
+
+    /**
+     * Remove all objects from the current share.
+     *
+     * @deprecated
+     *
+     * @return mixed  false if there is no IMAP connection, a PEAR
+     *                error if the objects could not be removed. True
+     *                if the call succeeded.
+     */
+    function removeAllObjects()
+    {
+        return $this->_storage->removeAllObjects();
+    }
+
+    /**
+     * Returns the groupware type of the given IMAP folder.
+     *
+     * @deprecated
+     *
+     * @param object $mailbox  The mailbox of interest.
+     *
+     * @return mixed  A string indicating the groupware type of $mailbox or
+     *                boolean "false" on error.
+     */
+    function getMailboxType($mailbox)
+    {
+        $list = &Kolab_List::singleton();
+        $folder = $list->getFolder($mailbox);
+        if (is_a($folder, 'PEAR_Error')) {
+            return $folder;
+        }
+        return $folder->getType();
+    }
+
+    /**
+     * Find the object using the given criteria in the current share.
+     *
+     * @deprecated
+     *
+     * @param string $criteria  The search criteria.
+     *
+     * @return mixed  false if no object can be found
+     */
+    function findObjects($criteria)
+    {
+        return $this->_storage->findObjects($criteria);
+    }
+
+    /**
+     * Return the MIME type of the message we are currently dealing with.
+     *
+     * @deprecated
+     *
+     * @return string  The MIME type of the message we are currently
+     *                 dealing with.
+     */
+    function getMimeType()
+    {
+        return $this->_mime_type;
+    }
+
+    function &getCurrentObject()
+    {
+        $element = $this->_xml->document_element();
+        return $element;
+    }
+
+    function &getElem($name, &$parent)
+    {
+        $elements = $this->getAllElems($name, $parent);
+
+        if (empty($elements)) {
+            $elements = false;
+            return $elements;
+        }
+
+        return $elements[0];
+    }
+
+    function &getAllElems($name, &$parent)
+    {
+        $elements = $parent->get_elements_by_tagname($name);
+        return $elements;
+    }
+
+    function &getRootElem($name)
+    {
+        $element = $this->getElem($name, $this->getCurrentObject());
+        return $element;
+    }
+
+    function &getAllRootElems($name)
+    {
+        $elements = $this->getAllElems($name, $this->getCurrentObject());
+        return $elements;
+    }
+
+    function delElem($name, &$parent)
+    {
+        $element = $this->getElem($name, $parent);
+        if ($element === false) {
+            return;
+        }
+
+        return $parent->remove_child($element);
+    }
+
+    function delAllElems($name, &$parent)
+    {
+        $elements = $this->getAllElems($name, $parent);
+        for ($i = 0, $j = count($elements); $i < $j; $i++) {
+            $parent->remove_child($elements[$i]);
+        }
+        return true;
+    }
+
+    function delAllRootElems($name)
+    {
+        return $this->delAllElems($name, $this->getCurrentObject());
+    }
+
+    function delRootElem(&$element)
+    {
+        if ($element === false) {
+            return;
+        }
+
+        $root = $this->getCurrentObject();
+        return $root->remove_child($element);
+    }
+
+    function getElemVal(&$parent, $name, $default = 0)
+    {
+        if ($parent === false) {
+            return $default;
+        }
+
+        $element = $this->getElem($name, $parent);
+        if ($element === false) {
+            return $default;
+        }
+
+        return $element->get_content();
+    }
+
+    function getElemStr(&$parent, $name, $default = '')
+    {
+        if ($parent === false) {
+            return $default;
+        }
+
+        $element = $this->getElem($name, $parent);
+        if ($element === false) {
+            return $default;
+        }
+
+        return Horde_String::convertCharset($element->get_content(), 'utf-8');
+    }
+
+    function getVal($name, $default = 0)
+    {
+        $val = $this->getElemVal($this->getCurrentObject(), $name, $default);
+        return $val;
+    }
+
+    function getStr($name, $default = '')
+    {
+        $str = $this->getElemStr($this->getCurrentObject(), $name, $default);
+        return $str;
+    }
+
+    function &initElem($name, &$parent)
+    {
+        if ($parent === false) {
+            $parent = $this->getCurrentObject();
+        }
+
+        $element = $this->getElem($name, $parent);
+
+        if ($element === false) {
+            $element = $parent->append_child($this->_xml->create_element($name));
+        }
+
+        $children = $element->child_nodes();
+        foreach ($children as $child) {
+            if ($child->node_type() == XML_TEXT_NODE) {
+                $element->remove_child($child);
+            }
+        }
+
+        return $element;
+    }
+
+    function &initRootElem($name)
+    {
+        $rootElement = $this->initElem($name, $this->getCurrentObject());
+        return $rootElement;
+    }
+
+    function &appendElem($name, &$parent)
+    {
+        $child = $parent->append_child($this->_xml->create_element($name));
+        return $child;
+    }
+
+    function &appendRootElem($name)
+    {
+        $append = $this->appendElem($name, $this->getCurrentObject());
+        return $append;
+    }
+
+    function &setElemVal(&$parent, $name, $value = '')
+    {
+        $element = $this->initElem($name, $parent);
+        $element->set_content($value);
+
+        return $element;
+    }
+
+    function &setElemStr(&$parent, $name, $value = '')
+    {
+        return $this->setElemVal($parent, $name, Horde_String::convertCharset($value, Horde_Nls::getCharset(), 'utf-8'));
+    }
+
+    function &setVal($name, $value = '')
+    {
+        $result = $this->setElemVal($this->getCurrentObject(), $name, $value);
+        return $result;
+    }
+
+    function &setStr($name, $value = '')
+    {
+        $result = $this->setElemStr($this->getCurrentObject(), $name, $value);
+        return $result;
+    }
+
+    /**
+     * Converts a string in the current character set to an IMAP UTF-7 string,
+     * suitable for use as the name of an IMAP folder.
+     *
+     * @deprecated
+     *
+     * @param string $name  The text in the current character set to convert.
+     *
+     * @return string  $name encoded in the IMAP variation of UTF-7.
+     */
+    function encodeImapFolderName($name)
+    {
+        return Horde_String::convertCharset($name, Horde_Nls::getCharset(), 'UTF7-IMAP');
+    }
+
+    /**
+     * Converts a string in the IMAP variation of UTF-7 into a string in the
+     * current character set.
+     *
+     * @deprecated
+     *
+     * @param string $name  The text in IMAP UTF-7 to convert.
+     *
+     * @return string  $name encoded in the current character set.
+     */
+    function decodeImapFolderName($name)
+    {
+        return Horde_String::convertCharset($name, 'UTF7-IMAP');
+    }
+
+    /**
+     * Converts all newlines (in DOS, MAC & UNIX format) in the specified text
+     * to unix-style (LF) format.
+     *
+     * @deprecated
+     *
+     * @param string $text  The text to convert.
+     *
+     * @return string  $text with all newlines replaced by LF.
+     */
+    function unixNewlines($text)
+    {
+        return preg_replace("/\r\n|\n|\r/s", "\n", $text);
+    }
+
+    /**
+     * Returns the unfolded representation of the given text.
+     *
+     * @deprecated
+     *
+     * @param string $text  The text to unfold.
+     *
+     * @return string  The unfolded representation of $text.
+     */
+    function unfoldText($text)
+    {
+        return preg_replace("/\r\n[ \t]+/", "", $text);
+    }
+
+    /**
+     * Returns a string containing the current UTC date in the format
+     * prescribed by the Kolab Format Specification.
+     *
+     * @deprecated
+     *
+     * @return string  The current UTC date in the format 'YYYY-MM-DD'.
+     */
+    function encodeDate($date = false)
+    {
+        return Horde_Kolab_Format_Date::encodeDate($date);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date string which is
+     * in the format prescribed by the Kolab Format Specification.
+     *
+     * @deprecated
+     *
+     * @param string $date  The string representation of the date.
+     *
+     * @return integer  The unix timestamp corresponding to $date.
+     */
+    function decodeDate($date)
+    {
+        return Horde_Kolab_Format_Date::decodeDate($date);
+    }
+
+    /**
+     * Returns a string containing the current UTC date and time in the format
+     * prescribed by the Kolab Format Specification.
+     *
+     * @deprecated
+     *
+     * @return string  The current UTC date and time in the format
+     *                 'YYYY-MM-DDThh:mm:ssZ', where the T and Z are literal
+     *                 characters.
+     */
+    function encodeDateTime($datetime = false)
+    {
+        return Horde_Kolab_Format_Date::encodeDateTime($datetime);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date-time string which
+     * is in the format prescribed by the Kolab Format Specification.
+     *
+     * @deprecated
+     *
+     * @param string $datetime  The string representation of the date & time.
+     *
+     * @return integer  The unix timestamp corresponding to $datetime.
+     */
+    function decodeDateTime($datetime)
+    {
+        return Horde_Kolab_Format_Date::decodeDateTime($datetime);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date or date-time
+     * string which is in either format prescribed by the Kolab Format
+     * Specification.
+     *
+     * @deprecated
+     *
+     * @param string $date  The string representation of the date (& time).
+     *
+     * @return integer  The unix timestamp corresponding to $date.
+     */
+    function decodeDateOrDateTime($date)
+    {
+        return Horde_Kolab_Format_Date::decodeDateOrDateTime($date);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date-time string which
+     * is in the format prescribed by the Kolab Format Specification.
+     *
+     * @deprecated
+     *
+     * @param string $date The string representation of the date (& time).
+     *
+     * @return integer  The unix timestamp corresponding to $datetime.
+     */
+    function decodeFullDayDate($date)
+    {
+        if (empty($date)) {
+            return 0;
+        }
+
+        return (strlen($date) == 10
+                ? Kolab::decodeDate($date) + 24 * 60 * 60
+                : Kolab::decodeDateTime($date));
+    }
+
+    function percentageToBoolean($percentage)
+    {
+        return $percentage == 100 ? '1' : '0';
+    }
+
+    function booleanToPercentage($boolean)
+    {
+        return $boolean ? '100' : '0';
+    }
+
+    /**
+     * Returns an array of application-specific constants, that are used in
+     * a generic manner throughout the library.
+     *
+     * @deprecated
+     *
+     * @param string $app  The application whose constants to query.
+     *
+     * @return mixed  An array of application-specific constants if $app is a
+     *                supported application, or a PEAR_Error object if $app is
+     *                not supported.
+     */
+    function getAppConsts($app)
+    {
+        switch ($app) {
+        case 'mnemo':
+            return array(
+                'folder_type'           => 'note',
+                'mime_type_suffix'      => 'note',
+                'allowed_types'         => array(
+                    'note',
+                ),
+                'default_folder_name'   => _("Notes"),
+                'application'           => $app,
+            );
+
+        case 'kronolith':
+            return array(
+                'folder_type'           => 'event',
+                'mime_type_suffix'      => 'event',
+                'allowed_types'         => array(
+                    'event',
+                ),
+                'default_folder_name'   => _("Calendar"),
+                'application'           => $app,
+            );
+
+        case 'turba':
+            return array(
+                'folder_type'           => 'contact',
+                'mime_type_suffix'      => 'contact',
+                'allowed_types'         => array(
+                    'contact',
+                    'distribution-list',
+                ),
+                'default_folder_name'   => _("Contacts"),
+                'application'           => $app,
+            );
+
+        case 'nag':
+            return array(
+                'folder_type'           => 'task',
+                'mime_type_suffix'      => 'task',
+                'allowed_types'         => array(
+                    'task',
+                ),
+                'default_folder_name'   => _("Tasks"),
+                'application'           => $app,
+            );
+
+        case 'h-prefs':
+            return array(
+                'folder_type'           => 'h-prefs',
+                'mime_type_suffix'      => 'h-prefs',
+                'allowed_types'         => array(
+                    'h-prefs',
+                ),
+                'default_folder_name'   => _("Preferences"),
+                'application'           => $app,
+            );
+
+        default:
+            return PEAR::raiseError(sprintf(_("The Horde/Kolab integration engine does not support \"%s\""), $app));
+        }
+    }
+
+    /**
+     * Returns the server url of the given type.
+     *
+     * This method is used to encapsulate multidomain support.
+     *
+     * @return string The server url or empty on error.
+     */
+    function getServer($server_type)
+    {
+        global $conf;
+
+        switch ($server_type) {
+        case 'imap':
+            return $conf['kolab']['imap']['server'];
+        case 'ldap':
+            return $conf['kolab']['ldap']['server'];
+        case 'smtp':
+            return $conf['kolab']['smtp']['server'];
+        default:
+            return '';
+        }
+    }
+
+    /**
+     * @deprecated
+     */
+    function triggerFreeBusyUpdate()
+    {
+    }
+}
diff --git a/framework/Kolab/package.xml b/framework/Kolab/package.xml
new file mode 100644 (file)
index 0000000..4863795
--- /dev/null
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Kolab</name>
+ <channel>pear.horde.org</channel>
+ <summary>Glue package for accessing a Kolab server from Horde</summary>
+ <description>This package contains a number of helper functions that
+ make communication between a Horde client and a Kolab server
+ smoother. It is now deprecated in favor of the various Kolab_*
+ packages.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>22:13:52</time>
+ <version>
+  <release>0.0.6</release>
+  <api>0.0.6</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Moved all functionality into separate Kolab_* modules.
+* Converted to package.xml 2.0 for pear.horde.org
+* Allow using either c-client or Net_IMAP for Kolab IMAP functionality
+  (Request #5124, wrobel@pardus.de)
+* Splitted Kolab date handling funktions into Date.php
+  (thomas.jarosch@intra2net.com)
+* Added XML handlers for the new Kolab framework.
+ </notes>
+ <contents>
+  <dir name="/" baseinstalldir="/Horde">
+   <file name="Kolab.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.5</release>
+    <api>0.0.5</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-09-27</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Yet Another Rewrite (tm). Kolab_Cyrus is gone. Support for the original
+Kolab server has been dropped - Kolab 2 is now the supported backend.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.4</release>
+    <api>0.0.4</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-04-21</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Pretty much a total reorganisation of the library; added a new Kolab_Cyrus
+object to better encapsulate communication with the Cyrus server. Moved and
+in most cases renamed existing functions to fit into the Kolab_Cyrus object,
+and added several new functions.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-03-09</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>- Modified the cyrusMailboxURI() function to properly handle shared
+  Cyrus folders.
+- Small change in the openCyrusConnection() function to disable creating
+  the specified folder if it is a shared folder, even if the user requests
+  otherwise.
+- Removed the compileFreeBusy() functions, as Kronolith:: already provides
+  a more robust version.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-30</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>- Replaced the various iconv() functions with Horde counterparts from
+  the MIME:: and String:: libraries.
+- Replaced the Net_HTTP_Client code with its equivalent that uses
+  HTTP_Request for GETs, and PHP streams with the WebDAV_Client wrapper
+  for WebDAV PUTs.
+- Fixed a small bug in openCyrusConnection() when false was being
+  returned (instead of raising Horde::fatal) on a certain error condition.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-28</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial contribution to Horde
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Kolab_Filter/COPYING b/framework/Kolab_Filter/COPYING
new file mode 100644 (file)
index 0000000..d1c6b98
--- /dev/null
@@ -0,0 +1,504 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+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 and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                 GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, 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 library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete 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 distribute a copy of this License along with the
+Library.
+
+  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.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, 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) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+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 Library, 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 Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you 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.
+
+  If distribution of 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 satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be 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.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library 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.
+
+  9. 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 Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+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 with
+this License.
+\f
+  11. 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 Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library 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 Library.
+
+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.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library 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.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser 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 Library
+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 Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+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
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "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
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. 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 LIBRARY 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
+LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  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 library's name and a brief idea of what it does.>
+    Copyright <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/framework/Kolab_Filter/TODO b/framework/Kolab_Filter/TODO
new file mode 100644 (file)
index 0000000..2e9f2eb
--- /dev/null
@@ -0,0 +1,34 @@
+====================================
+ Kolab_Filter Development TODO List
+====================================
+
+- Convert to Horde4/PHP5
+
+- Could the high speed parsing section be implemented in C?
+
+- Better man page (rewrite in XML)
+
+- Document the policies and the inner workings of the filters
+
+- Modify copyright statements to LGPL>=2.1
+
+- Clean the classes according to PEAR_CodeSniffer
+
+- Add real Kolab server tests that check a running system
+
+- Merge Horde_Kolab_Resource_Freebusy with Kolab_FreeBusy and
+  Kronolith_FreeBusy into a single Horde_Freebusy handler.
+
+- kolab/issue824 (kolabmailboxfilter run once for each recipient)
+  https://www.intevation.de/roundup/kolab/issue824
+
+- kolab/issue954 (kolab too permissive with faked from headers (but
+  rejects vaild one))
+  https://www.intevation.de/roundup/kolab/issue954
+
+- kolab/issue2833 (Mail filter setting "Always reject" should reject
+  instead of bounce)
+  https://www.intevation.de/roundup/kolab/issue2833
+
+- kolab/issue3287 (Document the Kolab_Filter policies)
+  https://www.intevation.de/roundup/kolab/issue3287
diff --git a/framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.1 b/framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.1
new file mode 100644 (file)
index 0000000..611659e
--- /dev/null
@@ -0,0 +1,168 @@
+.\" Automatically generated by Pod::Man 2.1801 (Pod::Simple 3.08)
+.\"
+.\" Standard preamble:
+.\" ========================================================================
+.de Sp \" Vertical space (when we can't use .PP)
+.if t .sp .5v
+.if n .sp
+..
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.\" Set up some character translations and predefined strings.  \*(-- will
+.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
+.\" double quote, and \*(R" will give a right double quote.  \*(C+ will
+.\" give a nicer C++.  Capital omega is used to do unbreakable dashes and
+.\" therefore won't be available.  \*(C` and \*(C' expand to `' in nroff,
+.\" nothing in troff, for use with C<>.
+.tr \(*W-
+.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
+.ie n \{\
+.    ds -- \(*W-
+.    ds PI pi
+.    if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
+.    if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\"  diablo 12 pitch
+.    ds L" ""
+.    ds R" ""
+.    ds C` ""
+.    ds C' ""
+'br\}
+.el\{\
+.    ds -- \|\(em\|
+.    ds PI \(*p
+.    ds L" ``
+.    ds R" ''
+'br\}
+.\"
+.\" Escape single quotes in literal strings from groff's Unicode transform.
+.ie \n(.g .ds Aq \(aq
+.el       .ds Aq '
+.\"
+.\" If the F register is turned on, we'll generate index entries on stderr for
+.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
+.\" entries marked with X<> in POD.  Of course, you'll have to process the
+.\" output yourself in some meaningful fashion.
+.ie \nF \{\
+.    de IX
+.    tm Index:\\$1\t\\n%\t"\\$2"
+..
+.    nr % 0
+.    rr F
+.\}
+.el \{\
+.    de IX
+..
+.\}
+.\"
+.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
+.\" Fear.  Run.  Save yourself.  No user-serviceable parts.
+.    \" fudge factors for nroff and troff
+.if n \{\
+.    ds #H 0
+.    ds #V .8m
+.    ds #F .3m
+.    ds #[ \f1
+.    ds #] \fP
+.\}
+.if t \{\
+.    ds #H ((1u-(\\\\n(.fu%2u))*.13m)
+.    ds #V .6m
+.    ds #F 0
+.    ds #[ \&
+.    ds #] \&
+.\}
+.    \" simple accents for nroff and troff
+.if n \{\
+.    ds ' \&
+.    ds ` \&
+.    ds ^ \&
+.    ds , \&
+.    ds ~ ~
+.    ds /
+.\}
+.if t \{\
+.    ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
+.    ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
+.    ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
+.    ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
+.    ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
+.    ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
+.\}
+.    \" troff and (daisy-wheel) nroff accents
+.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
+.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
+.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
+.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
+.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
+.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
+.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
+.ds ae a\h'-(\w'a'u*4/10)'e
+.ds Ae A\h'-(\w'A'u*4/10)'E
+.    \" corrections for vroff
+.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
+.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
+.    \" for low resolution devices (crt and lpr)
+.if \n(.H>23 .if \n(.V>19 \
+\{\
+.    ds : e
+.    ds 8 ss
+.    ds o a
+.    ds d- d\h'-1'\(ga
+.    ds D- D\h'-1'\(hy
+.    ds th \o'bp'
+.    ds Th \o'LP'
+.    ds ae ae
+.    ds Ae AE
+.\}
+.rm #[ #] #H #V #F C
+.\" ========================================================================
+.\"
+.IX Title "KOLABFILTER 1"
+.TH KOLABFILTER 1 "2009-02-23" "Kolab_Filter-0.1.4" "User Contributed PHP Documentation"
+.\" For nroff, turn off justification.  Always turn off hyphenation; it makes
+.\" way too many mistakes in technical documents.
+.if n .ad l
+.nh
+.SH "NAME"
+kolabfilter, kolabmailboxfilter \- Kolab filters
+.SH "SYNOPSIS"
+.IX Header "SYNOPSIS"
+\&\fBkolabfilter\fR \fB\-\-sender\fR=\fIsender\fR \fB\-\-recipient\fR=\fIrecipient\fR \fB\-\-host\fR=\fIhost\fR \fB\-\-client\fR=\fIclient\fR \fB\-\-user\fR=\fIuser\fR \fB\-\-config\fR=\fIconfig\fR
+.PP
+\&\fBkolabmailboxfilter\fR \fB\-\-sender\fR=\fIsender\fR \fB\-\-recipient\fR=\fIrecipient\fR \fB\-\-host\fR=\fIhost\fR \fB\-\-client\fR=\fIclient\fR \fB\-\-user\fR=\fIuser\fR \fB\-\-config\fR=\fIconfig\fR
+.SH "DESCRIPTION"
+.IX Header "DESCRIPTION"
+\&\fBkolabfilter\fR is a filter for outgoing mail on a Kolab Server. It rewrites headers and handles
+Outlook issues.
+.PP
+\&\fBkolabmailboxfilter\fR is a filter for incoming mail on a Kolab Server. It checks the messages for iCal
+data and handles automatic invitations.
+.SH "OPTIONS"
+.IX Header "OPTIONS"
+.IP "\fB\-s\fR \fIsender\fR|\fB\-\-sender\fR=\fIsender\fR" 8
+.IX Item "-s sender|--sender=sender"
+The message sender.
+.IP "\fB\-r\fR \fIrecipient\fR|\fB\-\-recipient\fR=\fIrecipient\fR" 8
+.IX Item "-r recipient|--recipient=recipient"
+A message recipient (can be repeated).
+.IP "\fB\-H\fR \fIhost\fR|\fB\-\-host\fR=\fIhost\fR" 8
+.IX Item "-H host|--host=host"
+The host running this script.
+.IP "\fB\-c\fR \fIclient\fR|\fB\-\-client\fR=\fIclient\fR" 8
+.IX Item "-c client|--client=client"
+The client sending the message.
+.IP "\fB\-u\fR \fIuser\fR|\fB\-\-user\fR=\fIuser\fR" 8
+.IX Item "-u user|--user=user"
+\&\s-1ID\s0 of the currently authenticated user.
+.IP "\fB\-C\fR \fIconfig\fR|\fB\-\-config\fR=\fIconfig\fR" 8
+.IX Item "-C config|--config=config"
+Path to the configuration file for this filter.
+.SH "COPYRIGHT AND AUTHORS"
+.IX Header "COPYRIGHT AND AUTHORS"
+Copyright 2004\-2009 Klarälvdalens Datakonsult \s-1AB\s0
diff --git a/framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.pod b/framework/Kolab_Filter/doc/Horde/Kolab/Filter/kolabfilter.pod
new file mode 100644 (file)
index 0000000..ec75dab
--- /dev/null
@@ -0,0 +1,52 @@
+=head1 NAME
+
+kolabfilter, kolabmailboxfilter - Kolab filters
+
+=head1 SYNOPSIS
+
+B<kolabfilter> B<--sender>=I<sender> B<--recipient>=I<recipient> B<--host>=I<host> B<--client>=I<client> B<--user>=I<user> B<--config>=I<config>
+
+B<kolabmailboxfilter> B<--sender>=I<sender> B<--recipient>=I<recipient> B<--host>=I<host> B<--client>=I<client> B<--user>=I<user> B<--config>=I<config>
+
+=head1 DESCRIPTION
+
+B<kolabfilter> is a filter for outgoing mail on a Kolab Server. It rewrites headers and handles
+Outlook issues.
+
+B<kolabmailboxfilter> is a filter for incoming mail on a Kolab Server. It checks the messages for iCal
+data and handles automatic invitations.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-s> I<sender>|B<--sender>=I<sender>
+
+The message sender.
+
+=item B<-r> I<recipient>|B<--recipient>=I<recipient>
+
+A message recipient (can be repeated).
+
+=item B<-H> I<host>|B<--host>=I<host>
+
+The host running this script.
+
+=item B<-c> I<client>|B<--client>=I<client>
+
+The client sending the message.
+
+=item B<-u> I<user>|B<--user>=I<user>
+
+ID of the currently authenticated user.
+
+=item B<-C> I<config>|B<--config>=I<config>
+
+Path to the configuration file for this filter.
+
+=back
+
+=head1 COPYRIGHT AND AUTHORS
+
+Copyright 2004-2009 Klarälvdalens Datakonsult AB
+
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Base.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Base.php
new file mode 100644 (file)
index 0000000..d8272d2
--- /dev/null
@@ -0,0 +1,309 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Base.php,v 1.10 2009/07/09 08:17:56 slusarz Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/** Load the required PEAR libraries */
+require_once 'PEAR.php';
+
+/** Console_Getopt */
+require_once 'Console/Getopt.php';
+
+/** Load the required Horde libraries */
+require_once 'Horde.php';
+
+/** Load the Filter libraries */
+require_once dirname(__FILE__) . '/Response.php';
+
+/** Load the argument parsing library */
+require_once 'Horde/Argv/Option.php';
+require_once 'Horde/Argv/OptionContainer.php';
+require_once 'Horde/Argv/HelpFormatter.php';
+require_once 'Horde/Argv/IndentedHelpFormatter.php';
+require_once 'Horde/Argv/Values.php';
+require_once 'Horde/Argv/Parser.php';
+
+/**
+ * A basic definition for a PHP based postfix filter.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Base.php,v 1.10 2009/07/09 08:17:56 slusarz Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Base
+{
+    /**
+     * The message ID.
+     *
+     * @var string
+     */
+    var $_id = '';
+
+    /**
+     * A temporary buffer file for storing the message.
+     *
+     * @var string
+     */
+    var $_tmpfile;
+
+    /**
+     * The file handle for the temporary file.
+     *
+     * @var int
+     */
+    var $_tmpfh;
+
+    /**
+     * The message sender.
+     *
+     * @var string
+     */
+    var $_sender;
+
+    /**
+     * The message recipients.
+     *
+     * @var array
+     */
+    var $_recipients = array();
+
+    /**
+     * The client host trying to send the message.
+     *
+     * @var string
+     */
+    var $_client_address;
+
+    /**
+     * The client host trying to send the message.
+     *
+     * @var string
+     */
+    var $_fqhostname;
+
+    /**
+     * The authenticated username of the sender.
+     *
+     * @var string
+     */
+    var $_sasl_username;
+
+    /**
+     * Initialize the class.
+     */
+    function init()
+    {
+        /* Parse our arguments */
+        $result = $this->_parseArgs();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+    }
+
+    /**
+     * Handle the message.
+     *
+     * @param int    $inh  The file handle pointing to the message.
+     * @param string $transport  The name of the transport driver.
+     */
+    function parse($inh = STDIN, $transport = null)
+    {
+        /* Setup the temporary storage */
+        $result = $this->_initTmp();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        Horde::logMessage(sprintf("%s starting up (sender=%s, recipients=%s, client_address=%s)", 
+                                  get_class($this), $this->_sender, 
+                                  join(', ',$this->_recipients), 
+                                  $this->_client_address),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_parse($inh, $transport);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        Horde::logMessage(sprintf("%s successfully completed (sender=%s, recipients=%s, client_address=%s, id=%s)", 
+                                  get_class($this), $this->_sender, 
+                                  join(', ',$this->_recipients), 
+                                  $this->_client_address, $this->_id),
+                          __FILE__, __LINE__, PEAR_LOG_INFO);
+    }
+
+    /**
+     * Creates a buffer for temporary storage of the message.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _initTmp()
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['tempdir'])) {
+            $tmpdir = $conf['kolab']['filter']['tempdir'];
+        } else {
+            $tmpdir = Horde::getTempDir();
+        }
+
+        /* Temp file for storing the message */
+        $this->_tmpfile = @tempnam($tmpdir, 'IN.' . get_class($this) . '.');
+        $this->_tmpfh = @fopen($this->_tmpfile, "w");
+        if( !$this->_tmpfh ) {
+            $msg = $php_errormsg;
+            return PEAR::raiseError(sprintf("Error: Could not open %s for writing: %s",
+                                            $this->_tmpfile, $msg),
+                                    OUT_LOG | EX_IOERR);
+        }
+
+        register_shutdown_function(array($this, '_cleanupTmp'));
+    }
+
+    /**
+     * A shutdown function for removing the temporary file.
+     */
+    function _cleanupTmp() {
+        if (@file_exists($this->_tmpfile)) {
+            @unlink($this->_tmpfile);
+        }
+    }
+
+    /**
+     * Parse the command line arguments provided to the filter and
+     * setup the class.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _parseArgs()
+    {
+        global $conf;
+
+        /* Get command line options. */
+        $p = new Horde_Kolab_Filter_Argv_Parser(
+            array('optionList' =>
+                  array(
+                      new Horde_Argv_Option('-s',
+                                            '--sender',
+                                            array('help' => 'The message sender.',
+                                                  'type' => 'string',
+                                                  'nargs' => 1)),
+                      new Horde_Argv_Option('-r',
+                                            '--recipient',
+                                            array('help' => 'A message recipient.',
+                                                  'action' => 'append',
+                                                  'type' => 'string')),
+                      new Horde_Argv_Option('-H',
+                                            '--host',
+                                            array('help' => 'The host running this script.')),
+                      new Horde_Argv_Option('-c',
+                                            '--client',
+                                            array('help' => 'The client sending the message.')),
+                      new Horde_Argv_Option('-u',
+                                            '--user',
+                                            array('help' => 'ID of the currently authenticated user.',
+                                                  'default' => '')),
+                      new Horde_Argv_Option('-C',
+                                            '--config',
+                                            array('help' => 'Path to the configuration file for this filter.'))
+                  )));
+
+        try {
+            list($values, $args) = $p->parseArgs();
+        } catch (InvalidArgumentException $e) {
+            $msg = $e->getMessage() . "\n\n" . $p->getUsage();
+            return PEAR::raiseError($msg, OUT_STDOUT | EX_USAGE);
+        }
+
+        if (!empty($values['config']) && file_exists($values['config'])) {
+            require_once $values['config'];
+        }
+
+        if (empty($values['recipient'])) {
+            $msg = 'Please provide one or more recipients.' 
+                . "\n\n" . $p->getUsage();
+            return PEAR::raiseError($msg, OUT_STDOUT | EX_USAGE);
+        }
+
+        $this->_sender = strtolower($values['sender']);
+        $this->_recipients = array_map('strtolower', $values['recipient']);
+        $this->_client_address = $values['client'];
+        $this->_fqhostname = strtolower($values['host']);
+        $this->_sasl_username = strtolower($values['user']);
+
+        Horde::logMessage(sprintf("Arguments: %s", print_r($values, true)),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        Horde_Nls::setCharset('utf-8');
+
+        if (!empty($conf['kolab']['filter']['locale_path'])
+            && !empty($conf['kolab']['filter']['locale'])) {
+            Horde_Nls::setTextdomain('Kolab_Filter', $conf['kolab']['filter']['locale_path'], Horde_Nls::getCharset());
+            setlocale(LC_ALL, $conf['kolab']['filter']['locale']);
+        }
+
+        /* This is used as the default domain for unqualified adresses */
+        global $_SERVER;
+        if (!array_key_exists('SERVER_NAME', $_SERVER)) {
+            $_SERVER['SERVER_NAME'] = $conf['kolab']['imap']['server'];
+        }
+
+        if (!array_key_exists('REMOTE_ADDR', $_SERVER)) {
+            $_SERVER['REMOTE_ADDR'] = $conf['kolab']['imap']['server'];
+        }
+
+        if (!array_key_exists('REMOTE_HOST', $_SERVER)) {
+            $_SERVER['REMOTE_HOST'] = $conf['kolab']['imap']['server'];
+        }
+
+        /* Always display all possible problems */
+        ini_set('error_reporting', E_ALL);
+        ini_set('track_errors', '1');
+
+        /* Setup error logging */
+        if (isset($conf['kolab']['filter']['error_log'])) {
+            ini_set('log_errors', '1');
+            ini_set('error_log', $conf['kolab']['filter']['error_log']);
+        }
+
+        /* Print PHP messages to StdOut if we are debugging */
+        if (isset($conf['kolab']['filter']['debug'])
+            && $conf['kolab']['filter']['debug']) {
+            ini_set('display_errors', '1');
+        }
+
+        /* Provide basic syslog debugging if nothing has been
+         * specified
+         */
+        if (!isset($conf['log'])) {
+            $conf['log']['enabled']          = true;
+            $conf['log']['priority']         = PEAR_LOG_DEBUG;
+            $conf['log']['type']             = 'syslog';
+            $conf['log']['name']             = LOG_MAIL;
+            $conf['log']['ident']            = 'kolabfilter';
+            $conf['log']['params']           = array();
+        }
+    }
+}
+
+class Horde_Kolab_Filter_Argv_Parser extends Horde_Argv_Parser 
+{
+    public function parserError($msg)
+    {
+        throw new InvalidArgumentException(sprintf("%s: error: %s\n", $this->getProgName(), $msg));
+    }
+
+    public function parserExit($status = 0, $msg = null)
+    {
+        throw new InvalidArgumentException(sprintf("%s: error: %s\n", $this->getProgName(), $msg));
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php
new file mode 100644 (file)
index 0000000..10eb878
--- /dev/null
@@ -0,0 +1,467 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php,v 1.12 2009-11-29 10:49:08 wrobel Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/** Load the basic filter definition */
+require_once dirname(__FILE__) . '/Base.php';
+
+/** Load the Transport library */
+require_once dirname(__FILE__) . '/Transport.php';
+
+define('RM_STATE_READING_HEADER', 1 );
+define('RM_STATE_READING_FROM',   2 );
+define('RM_STATE_READING_SUBJECT',3 );
+define('RM_STATE_READING_SENDER', 4 );
+define('RM_STATE_READING_BODY',   5 );
+
+/**
+ * A Kolab Server filter for outgoing mails.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php,v 1.12 2009-11-29 10:49:08 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Content extends Horde_Kolab_Filter_Base
+{
+    /**
+     * Handle the message.
+     *
+     * @param int    $inh  The file handle pointing to the message.
+     * @param string $transport  The name of the transport driver.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _parse($inh, $transport)
+    {
+        global $conf;
+
+        $result = $this->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (isset($conf['kolab']['filter']['verify_from_header'])) {
+            $verify_from_header = $conf['kolab']['filter']['verify_from_header'];
+        } else {
+            $verify_from_header = false;
+        }
+
+        if (isset($conf['kolab']['filter']['allow_sender_header'])) {
+            $allow_sender_header = $conf['kolab']['filter']['allow_sender_header'];
+        } else {
+            $allow_sender_header = false;
+        }
+
+        if (isset($conf['kolab']['filter']['allow_outlook_ical_forward'])) {
+            $allow_outlook_ical_forward = $conf['kolab']['filter']['allow_outlook_ical_forward'];
+        } else {
+            $allow_outlook_ical_forward = true;
+        }
+
+        if (empty($transport)) {
+            $transport = 'smtp';
+        }
+
+        $ical = false;
+        $from = false;
+        $subject = false;
+        $senderok = true;
+        $rewrittenfrom = false;
+        $state = RM_STATE_READING_HEADER;
+
+        while (!feof($inh) && $state != RM_STATE_READING_BODY) {
+
+            $buffer = fgets($inh, 8192);
+            $line = rtrim($buffer, "\r\n");
+
+            if ($line == '') {
+                /* Done with headers */
+                $state = RM_STATE_READING_BODY;
+                if ($from && $verify_from_header) {
+                    $rc = $this->_verify_sender($this->_sasl_username, $this->_sender, 
+                                                $from, $this->_client_address);
+                    if (is_a($rc, 'PEAR_Error')) {
+                        return $rc;
+                    } else if ($rc === true) {
+                        /* All OK, do nothing */
+                    } else if ($rc === false) {
+                        /* Reject! */
+                        $senderok = false;
+                    } else if (is_string($rc)) {
+                        /* Rewrite from */
+                        if (strpos($from, $rc) === false) {
+                            Horde::logMessage(sprintf("Rewriting '%s' to '%s'",
+                                                      $from, $rc), 
+                                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                            $rewrittenfrom = "From: $rc\r\n";
+                        }
+                    }
+                }
+            } else {
+                if ($line[0] != ' ' && $line[0] != "\t") {
+                    $state = RM_STATE_READING_HEADER;
+                }
+                switch( $state ) {
+                case RM_STATE_READING_HEADER:
+                    if ($allow_sender_header &&
+                        eregi('^Sender: (.*)', $line, $regs)) {
+                        $from = $regs[1];
+                        $state = RM_STATE_READING_SENDER;
+                    } else if (!$from && eregi('^From: (.*)', $line, $regs)) {
+                        $from = $regs[1];
+                        $state = RM_STATE_READING_FROM;
+                    } else if (eregi('^Subject: (.*)', $line, $regs)) {
+                        $subject = $regs[1];
+                        $state = RM_STATE_READING_SUBJECT;
+                    } else if (eregi('^Content-Type: text/calendar', $line)) {
+                        Horde::logMessage("Found iCal data in message",
+                                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                        $ical = true;
+                    } else if (eregi('^Message-ID: (.*)', $line, $regs)) {
+                        $this->_id = $regs[1];
+                    }
+                    break;
+                case RM_STATE_READING_FROM:
+                    $from .= $line;
+                    break;
+                case RM_STATE_READING_SENDER:
+                    $from .= $line;
+                    break;
+                case RM_STATE_READING_SUBJECT:
+                    $subject .= $line;
+                    break;
+                }
+            }
+            if (@fwrite($this->_tmpfh, $buffer) === false) {
+                $msg = $php_errormsg;
+                return PEAR::raiseError(sprintf("Error: Could not write to %s: %s",
+                                                $this->_tmpfile, $msg),
+                                        OUT_LOG | EX_IOERR);
+            }
+        }
+        while (!feof($inh)) {
+            $buffer = fread($inh, 8192);
+            if (@fwrite($this->_tmpfh, $buffer) === false) {
+                $msg = $php_errormsg;
+                return PEAR::raiseError(sprintf("Error: Could not write to %s: %s",
+                                                $this->_tmpfile, $msg),
+                                        OUT_LOG | EX_IOERR);
+            }
+        }
+
+        if (@fclose($this->_tmpfh) === false) {
+            $msg = $php_errormsg;
+            return PEAR::raiseError(sprintf("Error: Failed closing %s: %s",
+                                            $this->_tmpfile, $msg),
+                                    OUT_LOG | EX_IOERR);
+        }
+
+        if (!$senderok) {
+            if ($ical && $allow_outlook_ical_forward ) {
+                require_once(dirname(__FILE__) . '/Outlook.php');
+                $rc = Kolab_Filter_Outlook::embedICal($this->_fqhostname,
+                                                      $this->_sender,
+                                                      $this->_recipients,
+                                                      $from, $subject,
+                                                      $this->_tmpfile,
+                                                      $transport);
+                if (is_a($rc, 'PEAR_Error')) {
+                    return $rc;
+                } else if ($rc === true) {
+                    return;
+                }
+            } else {
+                return PEAR::raiseError(sprintf("Invalid From: header. %s looks like a forged sender",
+                                                $from),
+                                        OUT_LOG | OUT_STDOUT | EX_NOPERM);
+            }
+        }
+
+        $result = $this->_deliver($rewrittenfrom, $transport);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+    }
+
+    /**
+     * Deliver the message.
+     *
+     * @param string $transport  The name of the transport driver.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _deliver($rewrittenfrom, $transport)
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['smtp_host'])) {
+            $host = $conf['kolab']['filter']['smtp_host'];
+        } else {
+            $host = 'localhost';
+        }
+        if (isset($conf['kolab']['filter']['smtp_port'])) {
+            $port = $conf['kolab']['filter']['smtp_port'];
+        } else {
+            $port = 10025;
+        }
+
+        $transport = &Horde_Kolab_Filter_Transport::factory($transport, 
+                                               array('host' => $host, 
+                                                     'port' => $port));
+
+        $tmpf = @fopen($this->_tmpfile, 'r');
+        if (!$tmpf) {
+            $msg = $php_errormsg;
+            return PEAR::raiseError(sprintf("Error: Could not open %s for writing: %s",
+                                            $this->_tmpfile, $msg),
+                                    OUT_LOG | EX_IOERR);
+        }
+
+        $result = $transport->start($this->_sender, $this->_recipients);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $state = RM_STATE_READING_HEADER;
+        while (!feof($tmpf) && $state != RM_STATE_READING_BODY) {
+            $buffer = fgets($tmpf, 8192);
+            if ($rewrittenfrom) {
+                if (eregi( '^From: (.*)', $buffer)) {
+                    $result = $transport->data($rewrittenfrom);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                    $state = RM_STATE_READING_FROM;
+                    continue;
+                } else if ($state == RM_STATE_READING_FROM &&
+                           ($buffer[0] == ' ' || $buffer[0] == "\t")) {
+                    /* Folded From header, ignore */
+                    continue;
+                }
+            }
+            if (rtrim($buffer, "\r\n") == '') {
+                $state = RM_STATE_READING_BODY;
+            } else if ($buffer[0] != ' ' && $buffer[0] != "\t")  {
+                $state = RM_STATE_READING_HEADER;
+            }
+            $result = $transport->data($buffer);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        while (!feof($tmpf)) {
+            $buffer = fread($tmpf, 8192);
+            $len = strlen($buffer);
+
+            /* We can't tolerate that the buffer breaks the data
+             * between \r and \n, so we try to avoid that. The limit
+             * of 100 reads is to battle abuse
+             */
+            while ($buffer{$len-1} == "\r" && $len < 8192 + 100) {
+                $buffer .= fread($tmpf,1);
+                $len++;
+            }
+            $result = $transport->data($buffer);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        return $transport->end();
+    }
+
+    /**
+     * Check that the From header is not trying to impersonate a valid
+     * user that is not $sasluser.
+     *
+     * @param string $sasluser    The current, authenticated user.
+     * @param string $sender      Sender address
+     * @param string $fromhdr     From header
+     * @param string $client_addr Client IP
+     *
+     * @return mixed A PEAR_Error in case of an error, true if From
+     *               can be accepted, false if From must be rejected,
+     *               or a string with a corrected From header that
+     *               makes From acceptable
+     */
+    function _verify_sender($sasluser, $sender, $fromhdr, $client_addr) {
+
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['email_domain'])) {
+            $domains = $conf['kolab']['filter']['email_domain'];
+        } else {
+            $domains = 'localhost';
+        }
+
+        if (!is_array($domains)) {
+            $domains = array($domains);
+        }
+
+        if (isset($conf['kolab']['filter']['local_addr'])) {
+            $local_addr = $conf['kolab']['filter']['local_addr'];
+        } else {
+            $local_addr = '127.0.0.1';
+        }
+
+        if (empty($client_addr)) {
+            $client_addr = $local_addr;
+        }
+
+        if (isset($conf['kolab']['filter']['verify_subdomains'])) {
+            $verify_subdomains = $conf['kolab']['filter']['verify_subdomains'];
+        } else {
+            $verify_subdomains = true;
+        }
+
+        if (isset($conf['kolab']['filter']['reject_forged_from_header'])) {
+            $reject_forged_from_header = $conf['kolab']['filter']['reject_forged_from_header'];
+        } else {
+            $reject_forged_from_header = false;
+        }
+
+        if (isset($conf['kolab']['filter']['kolabhosts'])) {
+            $kolabhosts = $conf['kolab']['filter']['kolabhosts'];
+        } else {
+            $kolabhosts = 'localhost';
+        }
+
+        if (isset($conf['kolab']['filter']['privileged_networks'])) {
+            $privnetworks = $conf['kolab']['filter']['privileged_networks'];
+        } else {
+            $privnetworks = '127.0.0.0/8';
+        }
+
+        /* Allow anything from localhost and
+         * fellow Kolab-hosts 
+         */
+        if ($client_addr == $local_addr) {
+            return true;
+        }
+
+        $kolabhosts = explode(',', $kolabhosts);
+        $kolabhosts = array_map('gethostbyname', $kolabhosts );
+
+        $privnetworks = explode(',', $privnetworks);
+
+        if (array_search($client_addr, $kolabhosts) !== false) {
+            return true;
+        }
+
+        foreach ($privnetworks as $network) {
+
+            $iplong = ip2long($client_addr);
+            $cidr = explode("/", $network);
+            $netiplong = ip2long($cidr[0]);
+            if (count($cidr) == 2) {
+                $iplong = $iplong & (0xffffffff << 32 - $cidr[1]);
+                $netiplong = $netiplong & (0xffffffff << 32 - $cidr[1]);
+            }
+
+            if ($iplong == $netiplong) {
+                return true;
+            }
+        }
+
+        if ($sasluser) {
+            /* Load the Server library */
+            require_once 'Horde/Kolab/Server.php';
+
+            $server = &Horde_Kolab_Server::singleton();
+            if (is_a($server, 'PEAR_Error')) {
+                $server->code = OUT_LOG | EX_TEMPFAIL;
+                return $server;
+            }
+
+            $allowed_addrs = $server->addrsForIdOrMail($sasluser);
+            if (is_a($allowed_addrs, 'PEAR_Error')) {
+                $allowed_addrs->code = OUT_LOG | EX_NOUSER;
+                return $allowed_addrs;
+            }
+        } else {
+            $allowed_addrs = false;
+        }
+
+        if ($sasluser) {
+            if (isset($conf['kolab']['filter']['untrusted_subject_insert'])) {
+                $fmt = $conf['kolab']['filter']['untrusted_subject_insert'];
+            } else {
+                $fmt = _("(UNTRUSTED, sender is <%s>)");
+            }
+        } else {
+            if (isset($conf['kolab']['filter']['unauthenticated_subject_insert'])) {
+                $fmt = $conf['kolab']['filter']['unauthenticated_subject_insert'];
+            } else {
+                $fmt = _("(UNTRUSTED, sender <%s> is not authenticated)");
+            }
+        }
+
+        $adrs = imap_rfc822_parse_adrlist($fromhdr, $domains[0]);
+
+        foreach ($adrs as $adr) {
+            $from = $adr->mailbox . '@' . $adr->host;
+            $fromdom = $adr->host;
+
+            if ($sasluser) {
+                if (!in_array(strtolower($from), $allowed_addrs)) {
+                    Horde::logMessage(sprintf("%s is not an allowed From address for %s", 
+                                              $from, $sasluser), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    return false;
+                }
+            } else {
+                foreach ($domains as $domain) {
+                    if (strtolower($fromdom) == $domain 
+                        || ($verify_subdomains
+                            && substr($fromdom, -strlen($domain)-1) == ".$domain")) {
+                        if ($reject_forged_from_header) {
+                            Horde::logMessage(sprintf("%s is not an allowed From address for unauthenticated users.", 
+                                                      $from), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                            return false;
+                        } else {
+                            require_once 'Horde/String.php';
+                            require_once 'Horde/MIME.php';
+
+                            /* Rewrite */
+                            Horde::logMessage(sprintf("%s is not an allowed From address for unauthenticated users, rewriting.", 
+                                                      $from), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                            if (property_exists($adr, 'personal')) {
+                                $name = str_replace(array("\\", '"'), 
+                                                    array("\\\\",'\"'), 
+                                                    MIME::decode($adr->personal, 'utf-8'));
+                            } else {
+                                $name = '';
+                            }
+
+                            $untrusted = sprintf($fmt, $sender, $from, $name);
+
+                            // Is this test really correct?  Is $fromhdr a _decoded_ string?
+                            // If not comparing with the unencoded $untrusted is wrong.
+                            // sw - 20091125
+                            if (strpos( $fromhdr, $untrusted )===false) {
+                                $new_from = '"' . MIME::encode($untrusted) . '"';
+                                return  $new_from . ' <' . $from . '>';
+                            } else {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        /* All seems OK */
+        return true;
+    }
+}
+
+?>
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Exception.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Exception.php
new file mode 100644 (file)
index 0000000..0e1e211
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * A library for accessing the Kolab user database.
+ *
+ * PHP version 5
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Filter
+ */
+
+/**
+ * This class provides the standard error class for Kolab Filter exceptions.
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Filter
+ */
+class Horde_Kolab_Filter_Exception extends Horde_Exception
+{
+    /**
+     * Constants to define the error type.
+     */
+    const SYSTEM      = 1;
+    const NO_FREEBUSY = 2;
+
+    /**
+     * The array of available error messages. These are connected to the error
+     * codes used above and might be used to differentiate between what we show
+     * the user in the frontend and what we actually log in the backend.
+     *
+     * @var array
+     */
+    protected $messages;
+
+    /**
+     * Exception constructor
+     *
+     * @param mixed $message The exception message, a PEAR_Error object, or an
+     *                       Exception object.
+     * @param mixed $code    A numeric error code, or
+     *                       an array from error_get_last().
+     */
+    public function __construct($message = null, $code = null)
+    {
+        $this->setMessages();
+
+        parent::__construct($message, $code);
+    }
+
+    /**
+     * Initialize the messages handled by this exception.
+     *
+     * @return NULL
+     */
+    protected function setMessages()
+    {
+        $this->messages = array(
+            self::SYSTEM      => _("An internal error occured."),
+            self::NO_FREEBUSY => _("There is no free/busy data available."),
+        );
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Incoming.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Incoming.php
new file mode 100644 (file)
index 0000000..8cadcd9
--- /dev/null
@@ -0,0 +1,308 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Incoming.php,v 1.12 2009-11-16 20:02:56 wrobel Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/** Load the basic filter definition */
+require_once dirname(__FILE__) . '/Base.php';
+
+/** Load the Transport library */
+require_once dirname(__FILE__) . '/Transport.php';
+
+/**
+ * A Kolab Server filter for incoming mails that are parsed for iCal
+ * contents.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Incoming.php,v 1.12 2009-11-16 20:02:56 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Incoming extends Horde_Kolab_Filter_Base
+{
+
+    /**
+     * An array of headers to be added to the message
+     *
+     * @var array
+     */
+    var $_add_headers;
+
+    /**
+     * Handle the message.
+     *
+     * @param int    $inh        The file handle pointing to the message.
+     * @param string $transport  The name of the transport driver.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _parse($inh, $transport)
+    {
+        global $conf;
+
+        $result = $this->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (empty($transport)) {
+            if (isset($conf['kolab']['filter']['delivery_backend'])) {
+                $transport = $conf['kolab']['filter']['delivery_backend'];
+            } else {
+                $transport = 'lmtp';
+            }
+        }
+
+        $ical = false;
+        $add_headers = array();
+        $headers_done = false;
+
+        /* High speed section START */
+        $headers_done = false;
+        while (!feof($inh) && !$headers_done) {
+            $buffer = fgets($inh, 8192);
+            $line = rtrim( $buffer, "\r\n");
+            if ($line == '') {
+                /* Done with headers */
+                $headers_done = true;
+            } else if (eregi('^Content-Type: text/calendar', $line)) {
+                Horde::logMessage("Found iCal data in message",
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $ical = true;
+            } else if (eregi('^Message-ID: (.*)', $line, $regs)) {
+                $this->_id = $regs[1];
+            }
+            if (@fwrite($this->_tmpfh, $buffer) === false) {
+                $msg = $php_errormsg;
+                return PEAR::raiseError(sprintf("Error: Could not write to %s: %s",
+                                                $this->_tmpfile, $msg),
+                                        OUT_LOG | EX_IOERR);
+            }
+        }
+
+        if ($ical) {
+            /* iCal already identified. So let's just pipe the rest of
+             * the message through.
+             */
+            while (!feof($inh)) {
+                $buffer = fread($inh, 8192);
+                if (@fwrite($this->_tmpfh, $buffer) === false) {
+                    $msg = $php_errormsg;
+                    return PEAR::raiseError(sprintf("Error: Could not write to %s: %s",
+                                                    $this->_tmpfile, $msg),
+                                            OUT_LOG | EX_IOERR);
+                }
+            }
+        } else {
+            /* No ical yet? Let's try to identify the string
+             * "text/calendar". It's likely that we have a mime
+             * multipart message including iCal then.
+             */
+            while (!feof($inh)) {
+                $buffer = fread($inh, 8192);
+                if (@fwrite($this->_tmpfh, $buffer) === false) {
+                    $msg = $php_errormsg;
+                    return PEAR::raiseError(sprintf("Error: Could not write to %s: %s",
+                                                    $this->_tmpfile, $msg),
+                                            OUT_LOG | EX_IOERR);
+                }
+                if (strpos($buffer, 'text/calendar')) {
+                    $ical = true;
+                }
+            }
+        }
+        /* High speed section END */
+
+        if (@fclose($this->_tmpfh) === false) {
+            $msg = $php_errormsg;
+            return PEAR::raiseError(sprintf("Error: Failed closing %s: %s",
+                                            $this->_tmpfile, $msg),
+                                    OUT_LOG | EX_IOERR);
+        }
+
+        if ($ical) {
+            require_once 'Horde/Kolab/Resource.php';
+            $newrecips = array();
+            foreach ($this->_recipients as $recip) {
+                if (strpos($recip, '+')) {
+                    list($local, $rest)  = explode('+', $recip, 2);
+                    list($rest, $domain) = explode('@', $recip, 2);
+                    $resource = $local . '@' . $domain;
+                } else {
+                    $resource = $recip;
+                }
+                Horde::logMessage(sprintf("Calling resmgr_filter(%s, %s, %s, %s)",
+                                          $this->_fqhostname, $this->_sender,
+                                          $resource, $this->_tmpfile), __FILE__, __LINE__,
+                                  PEAR_LOG_DEBUG);
+                $r = new Kolab_Resource();
+                $rc = $r->handleMessage($this->_fqhostname, $this->_sender,
+                                        $resource, $this->_tmpfile);
+                $r->cleanup();
+                if (is_a($rc, 'PEAR_Error')) {
+                    return $rc;
+                } else if ($rc === true) {
+                    $newrecips[] = $resource;
+                }
+            }
+            $this->_recipients = $newrecips;
+            $this->_add_headers[] = 'X-Kolab-Scheduling-Message: TRUE';
+        } else {
+            $this->_add_headers[] = 'X-Kolab-Scheduling-Message: FALSE';
+        }
+
+        /* Check if we still have recipients */
+        if (empty($this->_recipients)) {
+            Horde::logMessage("No recipients left.",
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return;
+        } else {
+            $result = $this->_deliver($transport);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        Horde::logMessage("Filter_Incoming successfully completed.",
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+    }
+
+    /**
+     * Deliver the message.
+     *
+     * @param string $transport  The name of the transport driver.
+     *
+     * @return mixed A PEAR_Error in case of an error, nothing otherwise.
+     */
+    function _deliver($transport)
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['lmtp_host'])) {
+            $host = $conf['kolab']['filter']['lmtp_host'];
+        } else {
+            $host = 'localhost';
+        }
+        if (isset($conf['kolab']['filter']['lmtp_port'])) {
+            $port = $conf['kolab']['filter']['lmtp_port'];
+        } else {
+            $port = 2003;
+        }
+
+        /* Load the LDAP library */
+        require_once 'Horde/Kolab/Server.php';
+
+        $server = &Horde_Kolab_Server::singleton();
+        if (is_a($server, 'PEAR_Error')) {
+            $server->code = OUT_LOG | EX_TEMPFAIL;
+            return $server;
+        }
+
+        $hosts = array();
+        foreach ($this->_recipients as $recipient) {
+            if (strpos($recipient, '+')) {
+                list($local, $rest)  = explode('+', $recipient, 2);
+                list($rest, $domain) = explode('@', $recipient, 2);
+                $real_recipient = $local . '@' . $domain;
+            } else {
+                $real_recipient = $recipient;
+            }
+            $dn = $server->uidForIdOrMail($real_recipient);
+            if (is_a($dn, 'PEAR_Error')) {
+                return $dn;
+            }
+            if (!$dn) {
+                Horde::logMessage(sprintf('User %s does not exist!', $real_recipient),
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            }
+            try {
+                $user = $server->fetch($dn, 'Horde_Kolab_Server_Object_Kolab_User');
+            } catch (Horde_Kolab_Server_Exception $e) {
+                Horde::logMessage(sprintf('Failed fetching user object %s. Error was:',
+                                          $dn, $e->getMessage()),
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $user->code = OUT_LOG | EX_TEMPFAIL;
+                return $user;
+            }
+            $imapserver = $user->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IMAPHOST);
+            if (is_a($imapserver, 'PEAR_Error')) {
+                $imapserver->code = OUT_LOG | EX_TEMPFAIL;
+                return $imapserver;
+            }
+            if (!empty($imapserver)) {
+                $uhost = $imapserver;
+            } else {
+                $uhost = $host;
+            }
+            $hosts[$uhost][] = $recipient;
+        }
+
+        foreach (array_keys($hosts) as $imap_host) {
+            $params =  array('host' => $imap_host, 'port' => $port);
+            if ($imap_host != $host) {
+                $params['user'] = $conf['kolab']['filter']['lmtp_user'];
+                $params['pass'] = $conf['kolab']['filter']['lmtp_pass'];
+            }
+            $transport = &Horde_Kolab_Filter_Transport::factory($transport, $params);
+
+            $tmpf = @fopen($this->_tmpfile, 'r');
+            if (!$tmpf) {
+                $msg = $php_errormsg;
+                return PEAR::raiseError(sprintf("Error: Could not open %s for writing: %s",
+                                                $this->_tmpfile, $msg),
+                                        OUT_LOG | EX_IOERR);
+            }
+
+            $result = $transport->start($this->_sender, $hosts[$imap_host]);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $headers_done = false;
+            while (!feof($tmpf) && !$headers_done) {
+                $buffer = fgets($tmpf, 8192);
+                if (!$headers_done && rtrim($buffer, "\r\n") == '') {
+                    $headers_done = true;
+                    foreach ($this->_add_headers as $h) {
+                        $result = $transport->data("$h\r\n");
+                        if (is_a($result, 'PEAR_Error')) {
+                            return $result;
+                        }
+                    }
+                }
+                $result = $transport->data($buffer);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            while (!feof($tmpf)) {
+                $buffer = fread($tmpf, 8192);
+                $len = strlen($buffer);
+
+                /* We can't tolerate that the buffer breaks the data
+                 * between \r and \n, so we try to avoid that. The limit
+                 * of 100 reads is to battle abuse
+                 */
+                while ($buffer{$len-1} == "\r" && $len < 8192 + 100) {
+                    $buffer .= fread($tmpf, 1);
+                    $len++;
+                }
+                $result = $transport->data($buffer);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+            return $transport->end();
+        }
+    }
+}
+?>
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Outlook.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Outlook.php
new file mode 100644 (file)
index 0000000..55b1108
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Outlook.php,v 1.7 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/* Load the iCal handling */
+require_once 'Horde/iCalendar.php';
+
+/* Load MIME handlers */
+require_once 'Horde/MIME.php';
+require_once 'Horde/MIME/Message.php';
+require_once 'Horde/MIME/Headers.php';
+require_once 'Horde/MIME/Part.php';
+require_once 'Horde/MIME/Structure.php';
+
+/**
+ * Provides Mail rewriting for malformed Outlook messages
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Outlook.php,v 1.7 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Kolab_Filter_Outlook
+{
+
+    /**
+     * Returns a parsed MIME message
+     *
+     * @param string $text   The text of the message
+     *
+     * @return array An array with the MIME parsed headers and body.
+     */
+    function _mimeParse(&$text)
+    {
+        /* Taken from Horde's MIME/Structure.php */
+        require_once 'Mail/mimeDecode.php';
+
+        /* Set up the options for the mimeDecode class. */
+        $decode_args = array();
+        $decode_args['include_bodies'] = true;
+        $decode_args['decode_bodies'] = false;
+        $decode_args['decode_headers'] = false;
+
+        $mimeDecode = new Mail_mimeDecode($text, MIME_PART_EOL);
+        if (!($structure = $mimeDecode->decode($decode_args))) {
+            return false;
+        }
+
+        /* Put the object into imap_parsestructure() form. */
+        MIME_Structure::_convertMimeDecodeData($structure);
+
+        return array($structure->headers, $ret = &MIME_Structure::parse($structure));
+    }
+
+    /**
+     * Add a header entry.
+     *
+     * @param string        $name        The name of the header entry.
+     * @param MIME_Headers  $msg_header  A link to the MIME header handler.
+     * @param array         $headerarray The list of current headers.
+     */
+    function _copyHeader($name, &$msg_headers, &$headerarray)
+    {
+        $lname = strtolower($name);
+        if (array_key_exists($lname, $headerarray)) {
+            if (is_array($headerarray[$lname])) {
+                foreach ($headerarray[$lname] as $h) {
+                    $msg_headers->addHeader($name, $h);
+                }
+            } else {
+                $msg_headers->addHeader($name, $headerarray[$lname]);
+            }
+        }
+    }
+
+    /**
+     * Yet another problem: Outlook seems to remove the organizer from
+     * the iCal when forwarding -- we put the original sender back in
+     * as organizer.
+     *
+     * @param string        $icaltext  The ical message.
+     * @param MIME_Headers  $from      The message sender.
+     */
+    function _addOrganizer(&$icaltxt, $from)
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['email_domain'])) {
+            $email_domain = $conf['kolab']['filter']['email_domain'];
+        } else {
+            $email_domain = 'localhost';
+        }
+
+        $iCal = new Horde_iCalendar();
+        $iCal->parsevCalendar($icaltxt);
+        $vevent =& $iCal->findComponent('VEVENT');
+        if ($vevent) {
+            $organizer = $vevent->getAttribute('ORGANIZER', true);
+            if (is_a($organizer, 'PEAR_Error')) {
+                $adrs = imap_rfc822_parse_adrlist($from, $email_domain);
+                if (count($adrs) > 0) {
+                    $org_email = 'mailto:'.$adrs[0]->mailbox.'@'.$adrs[0]->host;
+                    $org_name  = $adrs[0]->personal;
+                    if ($org_name) {
+                        $vevent->setAttribute('ORGANIZER', $org_email,
+                                              array( 'CN' => $org_name), false);
+                    } else {
+                        $vevent->setAttribute('ORGANIZER', $org_email,
+                        array(), false);
+                    }
+                    Horde::logMessage(sprintf("Adding missing organizer '%s <%s>' to iCal.",
+                                              $org_name, $org_email),
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    $icaltxt = $iCal->exportvCalendar();
+                }
+            }
+        }
+    }
+
+    /**
+     * Yet another Outlook problem: Some versions of Outlook seems to be incapable
+     * of handling non-ascii characters properly in text/calendar parts of
+     * a multi-part/mixed mail which we use for forwarding.
+     * As a solution, we encode common characters as humanreadable
+     * two-letter ascii.
+     *
+     * @param string  $text  The message text.
+     *
+     * @return string The text with umlauts replaced.
+     */
+    function _recodeToAscii( $text ) {
+        $text = str_replace( ('æ'), 'ae', $text );
+        $text = str_replace( ('ø'), 'oe', $text );
+        $text = str_replace( ('Ã¥'), 'aa', $text );
+        $text = str_replace( ('ä'), 'ae', $text );
+        $text = str_replace( ('ö'), 'oe', $text );
+        $text = str_replace( ('ü'), 'ue', $text );
+        $text = str_replace( ('ß'), 'ss', $text );
+
+        $text = str_replace( ('Æ'), 'Ae', $text );
+        $text = str_replace( ('Ø'), 'Oe', $text );
+        $text = str_replace( ('Ã…'), 'Aa', $text );
+        $text = str_replace( ('Ä'), 'Ae', $text );
+        $text = str_replace( ('Ö'), 'Oe', $text );
+        $text = str_replace( ('Ü'), 'Ue', $text );
+
+        return $text;
+    }
+
+    /**
+     * Clean up iCal messages from Outlook.
+     *
+     * @param string  $fqhostname  The name of this host.
+     * @param string  $sender      The mail address of the sender.
+     * @param array   $recipients  The recipients of the message.
+     * @param string  $origfrom    The mail address of the original sender.
+     * @param string  $subject     The mail subject.
+     * @param string  $tmpfname    Path to the temporary message store.
+     *
+     * @return boolena|PEAR_Error True if the message was successfully rewritten.
+     */
+    function embedICal($fqhostname, $sender, $recipients, $origfrom, $subject,
+               $tmpfname, $transport)
+    {
+        Horde::logMessage(sprintf("Encapsulating iCal message forwarded by %s", $sender),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $forwardtext = "This is an invitation forwarded by outlook and\n".
+            "was rectified by the Kolab server.\n".
+            "The invitation was originally sent by\n%s.\n\n".
+            "Diese Einladung wurde von Outlook weitergeleitet\n".
+            "und vom Kolab-Server in gute Form gebracht.\n".
+            "Die Einladung wurde ursprünglich von\n%s geschickt.\n";
+
+        // Read in message text
+        $requestText = '';
+        $handle = @fopen($tmpfname, "r");
+        if ($handle === false) {
+            $msg = $php_errormsg;
+            return PEAR::raiseError(sprintf("Error: Could not open %s for writing: %s",
+                                            $tmpfname, $msg),
+                                    OUT_LOG | EX_IOERR);
+        }
+        while (!feof($handle)) {
+            $requestText .= fread($handle, 8192);
+        }
+        fclose($handle);
+
+        // Parse existing message
+        list( $headers, $mime) = Kolab_Filter_Outlook::_mimeParse($requestText);
+        $parts = $mime->contentTypeMap();
+        if (count($parts) != 1 || $parts[1] != 'text/calendar') {
+            Horde::logMessage("Message does not contain exactly one toplevel text/calendar part, passing through.",
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return false;
+        }
+        $basepart = $mime->getBasePart();
+
+        // Construct new MIME message with original message attached
+        $toppart = new MIME_Message();
+        $dorigfrom = Mail_mimeDecode::_decodeHeader($origfrom);
+        $textpart = new MIME_Part('text/plain', sprintf($forwardtext,$dorigfrom,$dorigfrom), 'UTF-8' );
+        $ical_txt = $basepart->transferDecode();
+        Kolab_Filter_Outlook::_addOrganizer($ical_txt, $dorigfrom);
+        $msgpart = new MIME_Part($basepart->getType(), Kolab_Filter_Outlook::_recodeToAscii($ical_txt),
+                                  $basepart->getCharset() );
+
+        $toppart->addPart($textpart);
+        $toppart->addPart($msgpart);
+
+        // Build the reply headers.
+        $msg_headers = new MIME_Headers();
+        Kolab_Filter_Outlook::_copyHeader( 'Received', $msg_headers, $headers );
+        //$msg_headers->addReceivedHeader();
+        $msg_headers->addMessageIdHeader();
+        Kolab_Filter_Outlook::_copyHeader( 'Date', $msg_headers, $headers );
+        Kolab_Filter_Outlook::_copyHeader( 'Resent-Date', $msg_headers, $headers );
+        Kolab_Filter_Outlook::_copyHeader( 'Subject', $msg_headers, $headers );
+        $msg_headers->addHeader('From', $sender);
+        $msg_headers->addHeader('To', join(', ', $recipients));
+        $msg_headers->addHeader('X-Kolab-Forwarded', 'TRUE');
+        $msg_headers->addMIMEHeaders($toppart);
+        Kolab_Filter_Outlook::_copyHeader( 'Content-Transfer-Encoding', $msg_headers, $headers );
+
+        if (is_object($msg_headers)) {
+            $headerArray = $toppart->encode($msg_headers->toArray(), $toppart->getCharset());
+        } else {
+            $headerArray = $toppart->encode($msg_headers, $toppart->getCharset());
+        }
+
+        return Kolab_Filter_Outlook::_inject($toppart, $recipients, $msg_headers, $sender, $transport);
+    }
+
+    function _inject(&$toppart, $recipients, $msg_headers, $sender, $transport)
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['smtp_host'])) {
+            $host = $conf['kolab']['filter']['smtp_host'];
+        } else {
+            $host = 'localhost';
+        }
+        if (isset($conf['kolab']['filter']['smtp_port'])) {
+            $port = $conf['kolab']['filter']['smtp_port'];
+        } else {
+            $port = 10025;
+        }
+
+        $transport = &Horde_Kolab_Filter_Transport::factory($transport,
+                                               array('host' => $host,
+                                                     'port' => $port));
+
+        $result = $transport->start($sender, $recipients);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $transport->data($msg_headers->toString() . $toppart->toString());
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $transport->end();
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Response.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Response.php
new file mode 100644 (file)
index 0000000..5df0304
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Response.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/* Require the PEAR library for PEAR errors */
+require_once 'PEAR.php';
+
+/* Some output constants */
+define( 'OUT_STDOUT', 128 );
+define( 'OUT_LOG', 256 );
+
+/* Failure constants from postfix src/global/sys_exits.h */
+define('EX_USAGE', 64);       /* command line usage error */
+define('EX_DATAERR', 65);     /* data format error */
+define('EX_NOINPUT', 66);     /* cannot open input */
+define('EX_NOUSER', 67);      /* user unknown */
+define('EX_NOHOST', 68);      /* host name unknown */
+define('EX_UNAVAILABLE', 69); /* service unavailable */
+define('EX_SOFTWARE', 70);    /* internal software error */
+define('EX_OSERR', 71);       /* system resource error */
+define('EX_OSFILE', 72);      /* critical OS file missing */
+define('EX_CANTCREAT', 73);   /* can't create user output file */
+define('EX_IOERR', 74);       /* input/output error */
+define('EX_TEMPFAIL', 75);    /* temporary failure */
+define('EX_PROTOCOL', 76);    /* remote error in protocol */
+define('EX_NOPERM', 77);      /* permission denied */
+define('EX_CONFIG', 78);      /* local configuration error */
+
+/**
+ * Provides error handling for the Kolab server filter scripts.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Response.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Response
+{
+
+    /**
+     * Constructor.
+     */
+    function Horde_Kolab_Filter_Response()
+    {
+        /* Set a custom PHP error handler to catch any coding errors */
+        set_error_handler(array($this, '_fatal'));
+    }
+
+    /**
+     * Handle the results of the message transport.
+     *
+     * @param mixed $result The reponse of the transport.
+     */
+    function handle($result)
+    {
+        /* No error? Be happy and exit clean */
+        if (!is_a($result, 'PEAR_Error')) {
+            exit(0);
+        }
+
+        $msg = $result->getMessage();
+        $code = $result->getCode();
+
+        if ($code & OUT_STDOUT) {
+            fwrite(STDOUT, $msg);
+        }
+        if  ($code & OUT_LOG || empty($code)) {
+            $this->_log($result);
+        }
+
+        // FIXME: Add a userinfo handler in case there were multiple
+        // combined errors
+
+        /* If we have an error code we want to return it to the
+         * calling application and exit here
+         */
+        if ($code) {
+            /* Return the first seven bits as error code to postfix */
+            exit($code & 127);
+        }
+    }
+
+    /**
+     * An alternative PHP error handler so that we don't drop silent
+     * on fatal errors.
+     *
+     * @param int    $errno    The error number.
+     * @param string $errmsg   The error message.
+     * @param string $filename The file where the error occured.
+     * @param int    $linenum  The line where the error occured.
+     * @param mixed  $vars     ?
+     *
+     * @return boolean Always false.
+     */
+    function _fatal($errno, $errmsg, $filename, $linenum, $vars)
+    {
+        /* Ignore strict errors for now since even PEAR will raise
+         * strict notices
+         */
+        if ($errno == E_STRICT) {
+            return false;
+        }
+
+        $fatal = array(E_ERROR,
+                       E_PARSE,
+                       E_CORE_ERROR,
+                       E_COMPILE_ERROR,
+                       E_USER_ERROR);
+
+        if (in_array($errno, $fatal)) {
+            $code = OUT_STDOUT | OUT_LOG | EX_UNAVAILABLE;
+            $msg = 'CRITICAL: You hit a fatal bug in kolab-filter. Please inform the Kolab developers at https://www.intevation.de/roundup/kolab/. The error was: ' . $errmsg;
+        } else {
+            $code = 0;
+            $msg = 'PHP Error: ' . $errmsg;
+        }
+
+        $error = new PEAR_Error($msg, $code);
+        $this->handle($error);
+
+        return false;
+    }
+
+    /**
+     * Log an error.
+     *
+     * @param PEAR_error $result The reponse of the transport.
+     */
+    function _log($result)
+    {
+        global $conf;
+
+        $msg = $result->getMessage() . '; Code: ' . $result->getCode();
+
+        /* Log all errors */
+        $file = __FILE__;
+        $line = __LINE__;
+
+        $frames = $result->getBacktrace();
+        if (count($frames) > 1) {
+            $frame = $frames[1];
+        } else if (count($frames) == 1) {
+            $frame = $frames[0];
+        }
+        if (isset($frame['file'])) {
+            $file = $frame['file'];
+        }
+        if (isset($frame['line'])) {
+            $line = $frame['line'];
+        }
+
+        /* In debugging mode the errors get delivered to the screen
+         * without a time stamp (mainly because of unit testing)
+         */
+        if (!isset($conf['kolab']['filter']['debug'])
+            || !$conf['kolab']['filter']['debug']) {
+            Horde::logMessage($msg, $file, $line, PEAR_LOG_ERR);
+        } else {
+            $msg .= ' (Line ' . $frame['line'] . ' in ' . basename($frame['file']) . ")\n";
+            fwrite(STDOUT, $msg);
+        }
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport.php
new file mode 100644 (file)
index 0000000..da120d9
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport.php,v 1.7 2009-11-29 10:12:59 wrobel Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Provides a delivery mechanism for a mail message.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport.php,v 1.7 2009-11-29 10:12:59 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport
+{
+    /**
+     * The connection parameters for the transport.
+     *
+     * @var array
+     */
+    var $_params;
+
+    /**
+     * The transport class delivering the message.
+     *
+     * @var mixed
+     */
+    var $_transport;
+
+    /**
+     * Internal marker to indicate if we received a new line.
+     *
+     * @var boolean
+     */
+    var $_got_newline;
+
+    /**
+     * Constructor.
+     */
+    function Horde_Kolab_Filter_Transport($params)
+    {
+        $this->_params = $params;
+        $this->_transport = false;
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Kolab_Filter_Transport instance based on $driver.
+     *
+     * @param string $driver The type of the concrete Horde_Kolab_Filter_Transport
+     *                       subclass to return.  The class name is
+     *                       based on the Horde_Kolab_Filter_Transport driver
+     *                       ($driver).  The code is dynamically
+     *                       included.
+     *
+     * @param array $params  A hash containing any additional
+     *                       configuration or connection parameters a
+     *                       subclass might need.
+     *
+     * @return Horde_Kolab_Filter_Transport|boolean The newly created concrete
+     *                                 Horde_Kolab_Filter_Transport instance, or
+     *                                 false on an error.
+     */
+    function &factory($driver, $params = array())
+    {
+        $class = 'Horde_Kolab_Filter_Transport_' . $driver;
+        if (!class_exists($class)) {
+            include dirname(__FILE__) . '/Transport/' . $driver . '.php';
+        }
+        if (class_exists($class)) {
+            $transport = new $class($params);
+            return $transport;
+        }
+        return PEAR::raiseError(sprintf('No such class \"%s\"', $class),
+                                OUT_LOG | EX_SOFTWARE);
+    }
+
+    /**
+     * Create the transport class.
+     */
+    function createTransport() {
+        $this->_transport = $this->_createTransport();
+    }
+
+    /**
+     * Starts transporting the message.
+     *
+     * @param string $sender The message sender.
+     * @param array $recips  The recipients of the message.
+     *
+     * @return boolean|PEAR_Error True on success, a PEAR_Error otherwise.
+     */
+    function start($sender, $recips)
+    {
+        $this->createTransport();
+
+        $myclass = get_class($this->_transport);
+        $this->_got_newline = true;
+
+        $result = $this->_transport->connect();
+        if (is_a($result, 'PEAR_Error')) {
+            $result->code = OUT_LOG | EX_UNAVAILABLE;
+            return $result;
+        }
+
+        if (isset($this->_params['user']) && isset($this->_params['pass']) ) {
+            $this->_transport->auth($this->_params['user'], $this->_params['pass'], 'PLAIN');
+        }
+
+        $result = $this->_transport->mailFrom($sender);
+        if (is_a($result, 'PEAR_Error')) {
+            $resp = $this->_transport->getResponse();
+            $error = PEAR::raiseError(sprintf('Failed to set sender: %s, code=%s',
+                                              $resp[1], $resp[0]), $resp[0]);
+            return $this->rewriteCode($error);
+        }
+
+        if (!is_array($recips)) {
+            $recips = array($recips);
+        }
+
+        $reciperrors = array();
+        foreach ($recips as $recip) {
+            $result = $this->_transport->rcptTo($recip);
+            if (is_a($result, 'PEAR_Error')) {
+                $resp = $this->_transport->getResponse();
+                $reciperrors[] = PEAR::raiseError(sprintf('Failed to set recipient: %s, code=%s',
+                                                          $resp[1], $resp[0]), $resp[0]);
+            }
+        }
+
+        if (count($reciperrors) == count($recips)) {
+            /* OK, all failed, just give up */
+            if (count($reciperrors) == 1) {
+                /* Only one failure, just return that */
+                return $this->rewriteCode($reciperrors[0]);
+            }
+            /* Multiple errors */
+            $error = $this->createErrorObject($reciperrors,
+                                              'Delivery to all recipients failed!');
+            return $this->rewriteCode($error);
+        }
+
+        $result = $this->_transport->_put('DATA');
+        if (is_a($result, 'PEAR_Error')) {
+            $resp = $this->_transport->getResponse();
+            $error = PEAR::raiseError(sprintf('Failed to send DATA: %s, code=%s',
+                                              $resp[1], $resp[0]), $resp[0]);
+            return $this->rewriteCode($error);
+        }
+
+        $result = $this->_transport->_parseResponse(354);
+        if (is_a($result, 'PEAR_Error')) {
+            return $this->rewriteCode($result);
+        }
+
+        if (!empty($reciperrors)) {
+            return $this->createErrorObject($reciperrors,
+                                            'Delivery to some recipients failed!');
+        }
+        return true;
+    }
+
+    /**
+     * Encapsulate multiple errors in one.
+     *
+     * @param array  $reciperrors  The errors.
+     * @param string $msg          A combined error message.
+     *
+     * @return PEAR_Error The combined error.
+     */
+    function createErrorObject($reciperrors, $msg = null)
+    {
+        /* Return the lowest errorcode to not bounce more
+         * than we have to
+         */
+        if ($msg == null) {
+            $msg = 'Delivery to recipients failed.';
+        }
+
+        $code = 1000;
+
+        foreach ($reciperrors as $err) {
+            if ($err->code < $code) {
+                $code = $err->code;
+            }
+        }
+        return new PEAR_Error($msg, $code, null, null, $reciperrors);
+    }
+
+    /**
+     * Modified implementation from Net_SMTP that supports dotstuffing
+     * even when getting the mail line-by line.
+     *
+     * @param string $data   Mail message data.
+     */
+    function quotedataline(&$data)
+    {
+        /*
+         * Change Unix (\n) and Mac (\r) linefeeds into Internet-standard CRLF
+         * (\r\n) linefeeds.
+         */
+        $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data);
+
+        /*
+         * Because a single leading period (.) signifies an end to the data,
+         * legitimate leading periods need to be "doubled" (e.g. '..').
+         */
+        if ($this->_got_newline && !empty($data) && $data[0] == '.') {
+            $data = '.'.$data;
+        }
+
+        $data = str_replace("\n.", "\n..", $data);
+        $len = strlen($data);
+        if ($len > 0) {
+            $this->_got_newline = ( $data[$len-1] == "\n" );
+        }
+    }
+
+    /**
+     * Send message data.
+     *
+     * @param string $data The text of the message.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function data($data) {
+        $this->quotedataline($data);
+        $result = $this->_transport->_send($data);
+        if (is_a($result, 'PEAR_Error')) {
+            $resp = $this->_transport->getResponse();
+            $error = PEAR::raiseError(sprintf('Failed to send message data: %s, code=%s',
+                                              $resp[1], $resp[0]), $resp[0]);
+            return $this->rewriteCode($error);
+        }
+        return true;
+    }
+
+    /**
+     * Finish sending data.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function end()
+    {
+        if ($this->_got_newline) {
+            $dot = ".\r\n";
+        } else {
+            $dot = "\r\n.\r\n";
+        }
+
+        $result = $this->_transport->_send($dot);
+        if (is_a($result, 'PEAR_Error')) {
+            $resp = $this->_transport->getResponse();
+            $error = PEAR::raiseError(sprintf('Failed to send message end: %s, code=%s',
+                                              $resp[1], $resp[0]), $resp[0]);
+            return $this->rewriteCode($error);
+        }
+        $result = $this->_transport->_parseResponse(250);
+        if (is_a($result, 'PEAR_Error')) {
+            return $this->rewriteCode($result);
+        }
+        $this->_transport->disconnect();
+        $this->_transport = false;
+        return true;
+    }
+
+    /**
+     * Rewrite the code to something postfix can understand.
+     *
+     * @param PEAR_error $result The reponse of the transport.
+     *
+     * @return PEAR_error An error with a rewritten error code.
+     */
+    function rewriteCode($result)
+    {
+        list($resultcode, $resultmessage) = $this->_transport->getResponse();
+        if ($resultcode < 500) {
+            $code = EX_TEMPFAIL;
+        } else {
+            $code = EX_UNAVAILABLE;
+        }
+        $append = sprintf(': %s, original code %s', $resultmessage, $resultcode);
+        $result->message = $result->getMessage() . $append;
+        $result->code = OUT_LOG | OUT_STDOUT | $code;
+        return $result;
+    }
+
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/DovecotLDA.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/DovecotLDA.php
new file mode 100644 (file)
index 0000000..96b2a58
--- /dev/null
@@ -0,0 +1,261 @@
+<?PHP
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/DovecotLDA.php,v 1.4 2008/12/12 15:24:04 wrobel Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Defines a transport mechanism for delivering mails to the dovecot
+ * IMAP server.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/DovecotLDA.php,v 1.4 2008/12/12 15:24:04 wrobel Exp $
+ *
+ * Copyright 2008 Intevation GmbH
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Sascha Wilde <wilde@intevation.de>
+ * @package Kolab_Filter
+ */
+class Dovecot_LDA
+{
+    /**
+     * The mail sender.
+     *
+     * @var string
+     */
+    var $_envelopeSender;
+
+    /**
+     * The mail recipient.
+     *
+     * @var string
+     */
+    var $_envelopeTo = array();
+
+    /**
+     * Transport status.
+     *
+     * @var int
+     */
+    var $_status;
+
+    /**
+     * The data that should be sent.
+     *
+     * @var array
+     */
+    var $_data;
+
+    /**
+     * File handle for delivery.
+     *
+     * @var int
+     */
+    var $_deliver_fh;
+
+    function Dovecot_LDA()
+    {
+        $this->_envelopeTo = false;
+        $this->_status = 220;
+    }
+
+    /**
+     * Pretends to connect to Dovecot which is not necessary.
+     *
+     * @return boolean|PEAR_Error Always true.
+     */
+    function connect()
+    {
+        global $conf;
+
+        if (!isset($conf['kolab']['filter']['dovecot_deliver'])) {
+            return PEAR::raiseError('Path to the dovecot delivery tool missing!');
+        }
+        return true;
+    }
+
+    /**
+     * Pretends to disconnect from Dovecot which is not necessary.
+     *
+     * @return boolean Always true.
+     */
+    function disconnect()
+    {
+        return true;
+    }
+
+
+    /**
+     * Set the mail sender.
+     *
+     * @return boolean Always true.
+     */
+    function mailFrom($sender)
+    {
+        $this->_envelopeSender = $sender;
+        $this->_status = 250;
+        return true;
+    }
+
+    /**
+     * Add a mail recipient.
+     *
+     * @return boolean Always true.
+     */
+    function rcptTo($rcpt)
+    {
+        $this->_envelopeTo[] = $rcpt;
+        $this->_status = 250;
+        return true;
+    }
+
+    /**
+     * Receive commands.
+     *
+     * @param string $cmd The command.
+     *
+     * @return boolean|PEAR_Error True if the command succeeded.
+     */
+    function _put($cmd)
+    {
+        if ($cmd == "DATA") {
+            $this->_status = 354;
+        } else {
+            $this->_status = 500;
+            return PEAR::raiseError('Dovecot LDA Backend received an unknown command.');
+        }
+        return true;
+    }
+
+    /**
+     * Check the current response code.
+     *
+     * @param string $code The response to parse.
+     *
+     * @return boolean|PEAR_Error True if the current status matches
+     * the expectation.
+     */
+    function _parseResponse($code)
+    {
+        if ($code) {
+            if ($this->_status == $code) {
+                return true;
+            } else {
+                return PEAR::raiseError(sprintf("Dovecot LDA status is %s though %s was expected!.",
+                                                $this->_status, $code));
+            }
+        } else {
+            return $this->status;
+        }
+    }
+
+    /**
+     * Send actual mail data.
+     *
+     * @param string $data The data to write.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _send($data)
+    {
+        $errors = array();
+        if ($data == ".\r\n" or $data == "\r\n.\r\n") {
+            foreach ($this->_envelopeTo as $recipient) {
+                $result = $this->_start_deliver($recipient);
+                if (is_a($result, 'PEAR_Error')) {
+                    $errors[] = $result;
+                    continue;
+                }
+
+                $result = $this->_deliver();
+                if (is_a($result, 'PEAR_Error')) {
+                    $errors[] = $result;
+                    continue;
+                }
+
+                $result = $this->_stop_deliver();
+                if (is_a($result, 'PEAR_Error')) {
+                    $errors[] = $result;
+                    continue;
+                }
+            }
+            if (empty($errors)) {
+                $this->_status = 250;
+            } else {
+                $this->_status = 500;
+                $msg = '';
+                foreach ($errors as $error) {
+                    $msg[] = $error->getMessage();
+                }
+                return PEAR::raiseError(sprintf("Dovecot delivery failed: %s",
+                                                join(', ', $msg)));
+            }
+        } else {
+            $this->_data[] = $data;
+        }
+        return true;
+    }
+
+
+    /**
+     * Start the delivery process for a recipient.
+     *
+     * @param string $recipient The recipient of the message.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _start_deliver($recipient)
+    {
+        global $conf;
+
+        Horde::logMessage(sprintf("Starting Dovecot delivery process with UID %d, GID %d (sender=%s, recipient=%s) ...",
+                                  getmyuid(), getmygid(),
+                                  $this->_envelopeSender, $recipient),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $deliver = $conf['kolab']['filter']['dovecot_deliver'];
+
+        $this->_deliver_fh = popen($deliver . ' -f ' . $this->_envelopeSender .
+                                   ' -d ' . $recipient, "w");
+        if ($this->_deliver_fh === false) {
+            return PEAR::raiseError('Failed to connect to the dovecot delivery tool!');
+        }
+        return true;
+    }
+
+    /**
+     * End the delivery process for a recipient.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _stop_deliver()
+    {
+        Horde::logMessage("Stoping Dovecot delivery process ...",
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $retval = pclose($this->_deliver_fh);
+        Horde::logMessage(sprintf("... return value was %d\n", $retval),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        if ($retval != 0) {
+            return PEAR::raiseError('Dovecot LDA Backend delivery process signaled an error.');
+        }
+        return true;
+    }
+
+    /**
+     * Write data to the deliver process.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _deliver()
+    {
+        foreach ($this->_data as $line) {
+            if (!fwrite($this->_deliver_fh, $line)) {
+                return PEAR::raiseError('Dovecot LDA Backend failed writing to the deliver process.');
+            }
+        }
+        return true;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/LMTPTLS.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/LMTPTLS.php
new file mode 100644 (file)
index 0000000..22abe94
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/LMTPTLS.php,v 1.3 2009/01/06 17:49:21 jan Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/* Require the base class */
+require_once 'Net/LMTP.php';
+
+/**
+ * Extended LMTP class with support for TLS.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Net_LMTP_TLS extends Net_LMTP {
+
+    /**
+     * Attempt to do LMTP authentication.
+     *
+     * @param string The userid to authenticate as.
+     * @param string The password to authenticate with.
+     * @param string The requested authentication method.  If none is
+     *               specified, the best supported method will be used.
+     *
+     * @return mixed Returns a PEAR_Error with an error message on any
+     *               kind of failure, or true on success.
+     * @access public
+     */
+    function auth($uid, $pwd , $method = '')
+    {
+        if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
+            if (!isset($this->_esmtp['STARTTLS'])) {
+                return PEAR::raiseError('LMTP server does not support authentication');
+            }
+            if (PEAR::isError($result = $this->_put('STARTTLS'))) {
+                return $result;
+            }
+            if (PEAR::isError($result = $this->_parseResponse(220))) {
+                return $result;
+            }
+            if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) {
+                return $result;
+            }
+            elseif ($result !== true) {
+                return PEAR::raiseError('STARTTLS failed');
+            }
+
+            /* Send LHLO again to recieve the AUTH string from the
+             * LMTP server. */
+            $this->_negotiate();
+            if (empty($this->_esmtp['AUTH'])) {
+                return PEAR::raiseError('LMTP server does not support authentication');
+            }
+        }
+
+        /*
+         * If no method has been specified, get the name of the best supported
+         * method advertised by the LMTP server.
+         */
+        if (empty($method) || $method === true ) {
+            if (PEAR::isError($method = $this->_getBestAuthMethod())) {
+                /* Return the PEAR_Error object from _getBestAuthMethod(). */
+                return $method;
+            } 
+        } else {
+            $method = strtoupper($method);
+        }
+
+        switch ($method) {
+            case 'DIGEST-MD5':
+                $result = $this->_authDigest_MD5($uid, $pwd);
+                break;
+            case 'CRAM-MD5':
+                $result = $this->_authCRAM_MD5($uid, $pwd);
+                break;
+            case 'LOGIN':
+                $result = $this->_authLogin($uid, $pwd);
+                break;
+            case 'PLAIN':
+                $result = $this->_authPlain($uid, $pwd);
+                break;
+            default : 
+                $result = new PEAR_Error("$method is not a supported authentication method");
+                break;
+        }
+
+        /* If an error was encountered, return the PEAR_Error object. */
+        if (PEAR::isError($result)) {
+            return $result;
+        }
+
+        /* RFC-2554 requires us to re-negotiate ESMTP after an AUTH. */
+        if (PEAR::isError($error = $this->_negotiate())) {
+            return $error;
+        }
+
+        return true;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php
new file mode 100644 (file)
index 0000000..bd0f2fd
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Drops a mail instead of delivering it.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_drop extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return DropWrapper Provides a null class as transport.
+     */
+    function &_createTransport()
+    {
+        $transport = new DropWrapper();
+        return $transport;
+    }
+}
+
+/**
+ * Defines a wrapper that provides functionality comparable to the
+ * Net/*MTP.php classes.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/drop.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class DropWrapper
+{
+    /**
+     * Pretends to connect.
+     *
+     * @return boolean Always true.
+     */
+    function connect()
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to disconnect.
+     *
+     * @return boolean Always true.
+     */
+    function disconnect()
+    {
+        return true;
+    }
+
+    /**
+     * Set the sender.
+     *
+     * @return boolean Always true.
+     */
+    function mailFrom($sender)
+    {
+        return true;
+    }
+
+    /**
+     * Set the recipient.
+     *
+     * @return boolean Always true.
+     */
+    function rcptTo($recipient)
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to send commands.
+     *
+     * @param string $cmd The command.
+     *
+     * @return boolean Always true.
+     */
+    function _put($cmd)
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to handle responses.
+     *
+     * @param string $code The response to parse.
+     *
+     * @return boolean Always true.
+     */
+    function _parseResponse($code)
+    {
+        return true;
+    }
+
+    /**
+     * Write data.
+     *
+     * @param string $data The data to write.
+     *
+     * @return boolean Always true.
+     */
+    function _send($data)
+    {
+        return true;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php
new file mode 100644 (file)
index 0000000..f509362
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Echos a mail for debugging.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_echo extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return StdOutWrapper Wraps STDOUT as transport
+     */
+    function &_createTransport()
+    {
+        $transport = new EchoWrapper();
+        return $transport;
+    }
+}
+
+/**
+ * Defines an echo wrapper that provides functionality comparable to
+ * the Net/*MTP.php classes.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/echo.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class EchoWrapper
+{
+    /**
+     * Pretends to connect to STDOUT.
+     *
+     * @return boolean Always true.
+     */
+    function connect()
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to disconnect from STDOUT.
+     *
+     * @return boolean Always true.
+     */
+    function disconnect()
+    {
+        return true;
+    }
+
+    /**
+     * Set the sender.
+     *
+     * @return mixed Result from writing the sender to STDOUT.
+     */
+    function mailFrom($sender)
+    {
+        echo sprintf("Mail from sender: %s\r\n", $sender);
+        return true;
+    }
+
+    /**
+     * Set the recipient.
+     *
+     * @return mixed Result from writing the recipient to STDOUT.
+     */
+    function rcptTo($recipient)
+    {
+        echo sprintf("Mail to recipient: %s\r\n", $recipient);
+        return true;
+    }
+
+    /**
+     * Pretends to send commands to STDOUT.
+     *
+     * @param string $cmd The command.
+     *
+     * @return boolean Always true.
+     */
+    function _put($cmd)
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to handle STDOUT responses.
+     *
+     * @param string $code The response to parse.
+     *
+     * @return boolean Always true.
+     */
+    function _parseResponse($code)
+    {
+        return true;
+    }
+
+    /**
+     * Echo data.
+     *
+     * @param string $data The data to write.
+     *
+     * @return mixed Result from writing data to STDOUT.
+     */
+    function _send($data)
+    {
+        echo $data;
+        return true;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lda.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lda.php
new file mode 100644 (file)
index 0000000..5c27e6d
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lda.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Provides DovecotLDA delivery.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lda.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Intevation GmbH
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Sascha Wilde <wilde@intevation.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_lda extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return DovecotLDA The LDA handler.
+     */
+    function &_createTransport()
+    {
+        require_once dirname(__FILE__) . '/DovecotLDA.php';
+
+        $transport = new Dovecot_LDA();
+
+        return $transport;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lmtp.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lmtp.php
new file mode 100644 (file)
index 0000000..7cbd789
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lmtp.php,v 1.4 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Provides LMTP for delivering a mail.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/lmtp.php,v 1.4 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_lmtp extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return Net_LMTP The LMTP handler.
+     */
+    function &_createTransport()
+    {
+        require_once dirname(__FILE__) . '/LMTPTLS.php';
+
+        if (!isset($this->_params['host'])) {
+            $this->_params['host'] = '127.0.0.1';
+        }
+
+        if (!isset($this->_params['port'])) {
+            $this->_params['port'] = 2003;
+        }
+
+        $transport = new Net_LMTP_TLS($this->_params['host'],
+                                       $this->_params['port']);
+
+        return $transport;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/smtp.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/smtp.php
new file mode 100644 (file)
index 0000000..18408de
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/smtp.php,v 1.4 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Provides SMTP for delivering mail.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/smtp.php,v 1.4 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_smtp extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return Net_SMTP The SMTP handler.
+     */
+    function &_createTransport()
+    {
+        require_once 'Net/SMTP.php';
+
+        if (!isset($this->_params['host'])) {
+            $this->_params['host'] = '127.0.0.1';
+        }
+
+        if (!isset($this->_params['port'])) {
+            $this->_params['port'] = 25;
+        }
+
+        $transport = new Net_SMTP($this->_params['host'],
+                                   $this->_params['port']);
+        return $transport;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php b/framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php
new file mode 100644 (file)
index 0000000..7f440c7
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * @package Kolab_Filter
+ */
+
+/**
+ * Delivers a mail to STDOUT for debugging.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class Horde_Kolab_Filter_Transport_stdout extends Horde_Kolab_Filter_Transport
+{
+    /**
+     * Create the transport handler.
+     *
+     * @return StdOutWrapper Wraps STDOUT as transport
+     */
+    function &_createTransport()
+    {
+        $transport = new StdOutWrapper();
+        return $transport;
+    }
+}
+
+/**
+ * Defines a STDOUT wrapper that provides functionality comparable to
+ * the Net/*MTP.php classes.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Transport/stdout.php,v 1.5 2009/07/14 00:28:32 mrubinsk Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Filter
+ */
+class StdOutWrapper
+{
+    /**
+     * Pretends to connect to STDOUT.
+     *
+     * @return boolean Always true.
+     */
+    function connect()
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to disconnect from STDOUT.
+     *
+     * @return boolean Always true.
+     */
+    function disconnect()
+    {
+        return true;
+    }
+
+    /**
+     * Set the sender.
+     *
+     * @return mixed Result from writing the sender to STDOUT.
+     */
+    function mailFrom($sender)
+    {
+        return fwrite(STDOUT, sprintf("Mail from sender: %s\n", $sender));
+    }
+
+    /**
+     * Set the recipient.
+     *
+     * @return mixed Result from writing the recipient to STDOUT.
+     */
+    function rcptTo($recipient)
+    {
+        return fwrite(STDOUT, sprintf("Mail to recipient: %s\n", $recipient));
+    }
+
+    /**
+     * Pretends to send commands to STDOUT.
+     *
+     * @param string $cmd The command.
+     *
+     * @return boolean Always true.
+     */
+    function _put($cmd)
+    {
+        return true;
+    }
+
+    /**
+     * Pretends to handle STDOUT responses.
+     *
+     * @param string $code The response to parse.
+     *
+     * @return boolean Always true.
+     */
+    function _parseResponse($code)
+    {
+        return true;
+    }
+
+    /**
+     * Write data to STDOUT.
+     *
+     * @param string $data The data to write.
+     *
+     * @return mixed Result from writing data to STDOUT.
+     */
+    function _send($data)
+    {
+        return fwrite(STDOUT, $data);
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Resource.php b/framework/Kolab_Filter/lib/Horde/Kolab/Resource.php
new file mode 100644 (file)
index 0000000..ee637a8
--- /dev/null
@@ -0,0 +1,1045 @@
+<?php
+/**
+ * Resource management for the Kolab server.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php,v 1.34 2009-12-03 14:41:03 wrobel Exp $
+ *
+ * PHP version 4
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+
+/** Load the iCal handling */
+require_once 'Horde/iCalendar.php';
+
+/** Load MIME handlers */
+require_once 'Horde/MIME.php';
+require_once 'Horde/MIME/Message.php';
+require_once 'Horde/MIME/Headers.php';
+require_once 'Horde/MIME/Part.php';
+require_once 'Horde/MIME/Structure.php';
+Horde_String::setDefaultCharset('utf-8');
+
+// What actions we can take when receiving an event request
+define('RM_ACT_ALWAYS_ACCEPT',              'ACT_ALWAYS_ACCEPT');
+define('RM_ACT_REJECT_IF_CONFLICTS',        'ACT_REJECT_IF_CONFLICTS');
+define('RM_ACT_MANUAL_IF_CONFLICTS',        'ACT_MANUAL_IF_CONFLICTS');
+define('RM_ACT_MANUAL',                     'ACT_MANUAL');
+define('RM_ACT_ALWAYS_REJECT',              'ACT_ALWAYS_REJECT');
+
+// What possible ITIP notification we can send
+define('RM_ITIP_DECLINE',                   1);
+define('RM_ITIP_ACCEPT',                    2);
+define('RM_ITIP_TENTATIVE',                 3);
+
+/**
+ * Provides Kolab resource handling
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php,v 1.34 2009-12-03 14:41:03 wrobel Exp $
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @package Kolab_Filter
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ */
+class Kolab_Resource
+{
+
+    /**
+     * Returns the resource policy applying for the given sender
+     *
+     * @param string $sender   The sender address
+     * @param string $resource The resource
+     *
+     * @return array|PEAR_Error An array with "cn", "home server" and the policy.
+     */
+    function _getResourceData($sender, $resource)
+    {
+        require_once 'Horde/Kolab/Server.php';
+        $db = &Horde_Kolab_Server::singleton();
+        if (is_a($db, 'PEAR_Error')) {
+            $db->code = OUT_LOG | EX_SOFTWARE;
+            return $db;
+        }
+
+        $dn = $db->uidForMail($resource, Horde_Kolab_Server_Object::RESULT_MANY);
+        if (is_a($dn, 'PEAR_Error')) {
+            $dn->code = OUT_LOG | EX_NOUSER;
+            return $dn;
+        }
+        if (is_array($dn)) {
+            if (count($dn) > 1) {
+                Horde::logMessage(sprintf("%s objects returned for %s",
+                                          $count($dn), $resource),
+                                  __FILE__, __LINE__, PEAR_LOG_WARNING);
+                return false;
+            } else {
+                $dn = $dn[0];
+            }
+        }
+        $user = $db->fetch($dn, 'Horde_Kolab_Server_Object_Kolab_User');
+
+        $cn      = $user->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN);
+        $id      = $user->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL);
+        $hs      = $user->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_HOMESERVER);
+        if (is_a($hs, 'PEAR_Error')) {
+            return $hs;
+        }
+        $hs      = strtolower($hs);
+        $actions = $user->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IPOLICY, false);
+        if (is_a($actions, 'PEAR_Error')) {
+            $actions->code = OUT_LOG | EX_UNAVAILABLE;
+            return $actions;
+        }
+        if ($actions === false) {
+            $actions = array(RM_ACT_MANUAL);
+        }
+
+        $policies = array();
+        $defaultpolicy = false;
+        foreach ($actions as $action) {
+            if (ereg('(.*):(.*)', $action, $regs)) {
+                $policies[strtolower($regs[1])] = $regs[2];
+            } else {
+                $defaultpolicy = $action;
+            }
+        }
+        // Find sender's policy
+        if (array_key_exists($sender, $policies)) {
+            // We have an exact match, stop processing
+            $action = $policies[$sender];
+        } else {
+            $action = false;
+            $dn = $db->uidForMailOrAlias($sender);
+            if (is_a($dn, 'PEAR_Error')) {
+                $dn->code = OUT_LOG | EX_NOUSER;
+                return $dn;
+            }
+            if ($dn) {
+                // Sender is local, check for groups
+                foreach ($policies as $gid => $policy) {
+                    if ($db->memberOfGroupAddress($dn, $gid)) {
+                        // User is member of group
+                        if (!$action) {
+                            $action = $policy;
+                        } else {
+                            $action = min($action, $policy);
+                        }
+                    }
+                }
+            }
+            if (!$action && $defaultpolicy) {
+                $action = $defaultpolicy;
+            }
+        }
+        return array('cn' => $cn, 'id' => $id,
+                     'homeserver' => $hs, 'action' => $action);
+    }
+
+    function &_getICal($filename)
+    {
+        $requestText = '';
+        $handle = fopen($filename, 'r');
+        while (!feof($handle)) {
+            $requestText .= fread($handle, 8192);
+        }
+
+        $mime = &MIME_Structure::parseTextMIMEMessage($requestText);
+
+        $parts = $mime->contentTypeMap();
+        foreach ($parts as $mimeid => $conttype) {
+            if ($conttype == 'text/calendar') {
+                $part = $mime->getPart($mimeid);
+
+                $iCalendar = new Horde_iCalendar();
+                $iCalendar->parsevCalendar($part->transferDecode());
+
+                return $iCalendar;
+            }
+        }
+        // No iCal found
+        return false;
+    }
+
+    function _imapConnect($id)
+    {
+        global $conf;
+
+        // Handle virtual domains
+        list($user, $domain) = explode('@', $id);
+        if (empty($domain)) {
+            $domain = $conf['kolab']['filter']['email_domain'];
+        }
+        $calendar_user = $conf['kolab']['filter']['calendar_id'] . '@' . $domain;
+
+        /* Load the authentication libraries */
+        $auth = Horde_Auth::singleton(isset($conf['auth']['driver'])?$conf['auth']['driver']:'kolab');
+        $authenticated = $auth->authenticate($calendar_user,
+                                             array('password' => $conf['kolab']['filter']['calendar_pass']),
+                                             false);
+
+        if (is_a($authenticated, 'PEAR_Error')) {
+            $authenticated->code = OUT_LOG | EX_UNAVAILABLE;
+            return $authenticated;
+        }
+        if (!$authenticated) {
+            return PEAR::raiseError(sprintf('Failed to authenticate as calendar user: %s',
+                                            $auth->getLogoutReasonString()),
+                                    OUT_LOG | EX_UNAVAILABLE);
+        }
+        @session_start();
+        $_SESSION['__auth'] = array(
+            'authenticated' => true,
+            'userId' => $calendar_user,
+            'timestamp' => time(),
+            'credentials' => Horde_Secret::write(Horde_Secret::getKey('auth'),
+                                           serialize(array('password' => $conf['kolab']['filter']['calendar_pass']))),
+            'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
+        );
+
+        /* Kolab IMAP handling */
+        require_once 'Horde/Kolab/Storage/List.php';
+        $list = &Kolab_List::singleton();
+        $default = $list->getForeignDefault($id, 'event');
+        if (!$default || is_a($default, 'PEAR_Error')) {
+            $default = &new Kolab_Folder();
+            $default->setList($list);
+            $default->setName($conf['kolab']['filter']['calendar_store']);
+            //FIXME: The calendar user needs access here
+            $attributes = array('default' => true,
+                                'type' => 'event',
+                                'owner' => $id);
+            $result = $default->save($attributes);
+            if (is_a($result, 'PEAR_Error')) {
+                $result->code = OUT_LOG | EX_UNAVAILABLE;
+                return $result;
+            }
+        }
+        return $default;
+    }
+
+    function _objectFromItip(&$itip)
+    {
+        $object = array();
+        $object['uid'] = $itip->getAttributeDefault('UID', '');
+
+        $org_params = $itip->getAttribute('ORGANIZER', true);
+        if (!is_a( $org_params, 'PEAR_Error')) {
+            if (!empty($org_params[0]['CN'])) {
+                $object['organizer']['display-name'] = $org_params[0]['CN'];
+            }
+            $orgemail = $itip->getAttributeDefault('ORGANIZER', '');
+            if (eregi('mailto:(.*)', $orgemail, $regs )) {
+                $orgemail = $regs[1];
+            }
+            $object['organizer']['smtp-address'] = $orgemail;
+        }
+        $object['summary'] = $itip->getAttributeDefault('SUMMARY', '');
+        $object['location'] = $itip->getAttributeDefault('LOCATION', '');
+        $object['body'] = $itip->getAttributeDefault('DESCRIPTION', '');
+
+        $dtend = $itip->getAttributeDefault('DTEND', '');
+        if (is_array($dtend)) {
+            $object['_is_all_day'] = true;
+        }
+        $object['start-date'] = $this->convert2epoch($itip->getAttributeDefault('DTSTART', ''));
+        $object['end-date'] = $this->convert2epoch($dtend);
+
+        $attendees = $itip->getAttribute('ATTENDEE');
+        if (!is_a( $attendees, 'PEAR_Error')) {
+            $attendees_params = $itip->getAttribute('ATTENDEE', true);
+            if (!is_array($attendees)) {
+                $attendees = array($attendees);
+            }
+            if (!is_array($attendees_params)) {
+                $attendees_params = array($attendees_params);
+            }
+
+            $object['attendee'] = array();
+            for ($i = 0; $i < count($attendees); $i++) {
+                $attendee = array();
+                if (isset($attendees_params[$i]['CN'])) {
+                    $attendee['display-name'] = $attendees_params[$i]['CN'];
+                }
+
+                $attendeeemail = $attendees[$i];
+                if (eregi('mailto:(.*)', $attendeeemail, $regs)) {
+                    $attendeeemail = $regs[1];
+                }
+                $attendee['smtp-address'] = $attendeeemail;
+
+                if( $attendees_params[$i]['RSVP'] == 'FALSE' ) {
+                    $attendee['request-response'] = false;
+                } else {
+                    $attendee['request-response'] = true;
+                }
+
+                if (isset($attendees_params[$i]['ROLE'])) {
+                    $attendee['role'] = $attendees_params[$i]['ROLE'];
+                }
+
+                if (isset($attendees_params[$i]['PARTSTAT'])) {
+                    $status = strtolower($attendees_params[$i]['PARTSTAT']);
+                    switch ($status) {
+                    case 'needs-action':
+                    case 'delegated':
+                        $attendee['status'] = 'none';
+                        break;
+                    default:
+                        $attendee['status'] = $status;
+                        break;
+                    }
+                }
+
+                $object['attendee'][] = $attendee;
+            }
+        }
+
+        // Alarm
+        $valarm = $itip->findComponent('VALARM');
+        if ($valarm) {
+            $trigger = $valarm->getAttribute('TRIGGER');
+            if (!is_a($trigger, 'PEAR_Error')) {
+                $p = $valarm->getAttribute('TRIGGER', true);
+                if ($trigger < 0) {
+                    // All OK, enter the alarm into the XML
+                    // NOTE: The Kolab XML format seems underspecified
+                    // wrt. alarms currently...
+                    $object['alarm'] = -$trigger / 60;
+                }
+            } else {
+                Horde::logMessage('No TRIGGER in VALARM. ' . $trigger->getMessage(),
+                                  __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        // Recurrence
+        $rrule_str = $itip->getAttribute('RRULE');
+        if (!is_a($rrule_str, 'PEAR_Error')) {
+            require_once 'Horde/Date/Recurrence.php';
+            $recurrence = new Horde_Date_Recurrence(time());
+            $recurrence->fromRRule20($rrule_str);
+            $object['recurrence'] = $recurrence->toHash();
+        }
+
+        Horde::logMessage(sprintf('Assembled event object: %s',
+                                  print_r($object, true)),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $object;
+    }
+
+    function handleMessage($fqhostname, $sender, $resource, $tmpfname)
+    {
+        global $conf;
+
+        $rdata = $this->_getResourceData($sender, $resource);
+        if (is_a($rdata, 'PEAR_Error')) {
+            return $rdata;
+        } else if ($rdata === false) {
+            /* No data, probably not a local user */
+            return true;
+        } else if ($rdata['homeserver'] && $rdata['homeserver'] != $fqhostname) {
+            /* Not the users homeserver, ignore */
+            return true;
+        }
+
+        $cn = $rdata['cn'];
+        $id = $rdata['id'];
+        if (isset($rdata['action'])) {
+            $action = $rdata['action'];
+        } else {
+            // Manual is the only safe default!
+            $action = RM_ACT_MANUAL;
+        }
+        Horde::logMessage(sprintf('Action for %s is %s',
+                                  $sender, $action),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // Get out as early as possible if manual
+        if ($action == RM_ACT_MANUAL) {
+            Horde::logMessage(sprintf('Passing through message to %s', $id),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+
+        /* Get the iCalendar data (i.e. the iTip request) */
+        $iCalendar = &$this->_getICal($tmpfname);
+        if ($iCalendar === false) {
+            // No iCal in mail
+            Horde::logMessage(sprintf('Could not parse iCalendar data, passing through to %s', $id),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+        // Get the event details out of the iTip request
+        $itip = &$iCalendar->findComponent('VEVENT');
+        if ($itip === false) {
+            Horde::logMessage(sprintf('No VEVENT found in iCalendar data, passing through to %s', $id),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+
+        // What is the request's method? i.e. should we create a new event/cancel an
+        // existing event, etc.
+        $method = strtoupper($iCalendar->getAttributeDefault('METHOD',
+                                                             $itip->getAttributeDefault('METHOD', 'REQUEST')));
+
+        // What resource are we managing?
+        Horde::logMessage(sprintf('Processing %s method for %s', $method, $id),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // This is assumed to be constant across event creation/modification/deletipn
+        $uid = $itip->getAttributeDefault('UID', '');
+        Horde::logMessage(sprintf('Event has UID %s', $uid),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // Who is the organiser?
+        $organiser = preg_replace('/^mailto:\s*/i', '', $itip->getAttributeDefault('ORGANIZER', ''));
+        Horde::logMessage(sprintf('Request made by %s', $organiser),
+                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // What is the events summary?
+        $summary = $itip->getAttributeDefault('SUMMARY', '');
+
+        $dtstart = $this->convert2epoch($itip->getAttributeDefault('DTSTART', 0));
+        $dtend = $this->convert2epoch($itip->getAttributeDefault('DTEND', 0));
+
+        Horde::logMessage(sprintf('Event starts on <%s> %s and ends on <%s> %s.',
+                                  $dtstart, $this->iCalDate2Kolab($dtstart), $dtend, $this->iCalDate2Kolab($dtend)),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        if ($action == RM_ACT_ALWAYS_REJECT) {
+            if ($method == 'REQUEST') {
+                Horde::logMessage(sprintf('Rejecting %s method', $method),
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+                $this->sendITipReply($cn, $resource, $itip, RM_ITIP_DECLINE,
+                                     $organiser, $uid, $is_update);
+                return false;
+            } else {
+                Horde::logMessage(sprintf('Passing through %s method for ACT_ALWAYS_REJECT policy', $method),
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+                return true;
+            }
+        }
+
+        $is_update  = false;
+        $imap_error = false;
+        $ignore     = array();
+
+        $folder = $this->_imapConnect($id);
+        if (is_a($folder, 'PEAR_Error')) {
+            $imap_error = &$folder;
+        }
+        if (!is_a($imap_error, 'PEAR_Error') && !$folder->exists()) {
+            $imap_error = &PEAR::raiseError('Error, could not open calendar folder!',
+                                    OUT_LOG | EX_TEMPFAIL);
+        }
+
+        if (!is_a($imap_error, 'PEAR_Error')) {
+            $data = $folder->getData();
+            if (is_a($data, 'PEAR_Error')) {
+                $imap_error = &$data;
+            }
+        }
+
+        if (is_a($imap_error, 'PEAR_Error')) {
+            Horde::logMessage(sprintf('Failed accessing IMAP calendar: %s',
+                                      $folder->getMessage()),
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            if ($action == RM_ACT_MANUAL_IF_CONFLICTS) {
+                return true;
+            }
+        }
+
+        switch ($method) {
+        case 'REQUEST':
+            if ($action == RM_ACT_MANUAL) {
+                Horde::logMessage(sprintf('Passing through %s method', $method),
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+                break;
+            }
+
+            if (is_a($imap_error, 'PEAR_Error') || !$data->objectUidExists($uid)) {
+                $old_uid = null;
+            } else {
+                $old_uid = $uid;
+                $ignore[] = $uid;
+                $is_update = true;
+            }
+
+            /** Generate the Kolab object */
+            $object = $this->_objectFromItip($itip);
+
+            $outofperiod=0;
+
+            // Don't even bother checking free/busy info if RM_ACT_ALWAYS_ACCEPT
+            // is specified
+            if ($action != RM_ACT_ALWAYS_ACCEPT) {
+
+                try {
+                    require_once 'Horde/Kolab/Resource/Freebusy.php';
+                    $fb  = Horde_Kolab_Resource_Freebusy::singleton();
+                    $vfb = $fb->get($resource);
+                } catch (Exception $e) {
+                    return PEAR::raiseError($e->getMessage(),
+                                            OUT_LOG | EX_UNAVAILABLE);
+                }
+
+                $vfbstart = $vfb->getAttributeDefault('DTSTART', 0);
+                $vfbend = $vfb->getAttributeDefault('DTEND', 0);
+                Horde::logMessage(sprintf('Free/busy info starts on <%s> %s and ends on <%s> %s',
+                                          $vfbstart, $this->iCalDate2Kolab($vfbstart), $vfbend, $this->iCalDate2Kolab($vfbend)),
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                if ($vfbstart && $dtstart > $this->convert2epoch ($vfbend)) {
+                    $outofperiod=1;
+                } else {
+                    // Check whether we are busy or not
+                    $busyperiods = $vfb->getBusyPeriods();
+                    Horde::logMessage(sprintf('Busyperiods: %s',
+                                              print_r($busyperiods, true)),
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    $extraparams = $vfb->getExtraParams();
+                    Horde::logMessage(sprintf('Extraparams: %s',
+                                              print_r($extraparams, true)),
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    $conflict = false;
+                    if (!empty($object['recurrence'])) {
+                        $recurrence = new Horde_Date_Recurrence(time());
+                        $recurrence->fromHash($object['recurrence']);
+                        $duration = $dtend - $dtstart;
+                        $events = array();
+                        $next_start = $vfbstart;
+                        $next = $recurrence->nextActiveRecurrence($vfbstart);
+                        while ($next !== false && $next->compareDate($vfbend) <= 0) {
+                            $next_ts = $next->timestamp();
+                            $events[$next_ts] = $next_ts + $duration;
+                            $next = $recurrence->nextActiveRecurrence(array('year' => $next->year,
+                                                                            'month' => $next->month,
+                                                                            'mday' => $next->mday + 1,
+                                                                            'hour' => $next->hour,
+                                                                            'min' => $next->min,
+                                                                            'sec' => $next->sec));
+                        }
+                    } else {
+                        $events = array($dtstart => $dtend);
+                    }
+
+                    foreach ($events as $dtstart => $dtend) {
+                        foreach ($busyperiods as $busyfrom => $busyto) {
+                            if (empty($busyfrom) && empty($busyto)) {
+                                continue;
+                            }
+                            Horde::logMessage(sprintf('Busy period from %s to %s',
+                                                      strftime('%a, %d %b %Y %H:%M:%S %z', $busyfrom),
+                                                      strftime('%a, %d %b %Y %H:%M:%S %z', $busyto)
+                                              ),
+                                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                            if ((isset($extraparams[$busyfrom]['X-UID'])
+                                 && in_array(base64_decode($extraparams[$busyfrom]['X-UID']), $ignore))
+                                || (isset($extraparams[$busyfrom]['X-SID'])
+                                    && in_array(base64_decode($extraparams[$busyfrom]['X-SID']), $ignore))) {
+                                // Ignore
+                                continue;
+                            }
+                            if (($busyfrom >= $dtstart && $busyfrom < $dtend) || ($dtstart >= $busyfrom && $dtstart < $busyto)) {
+                                Horde::logMessage('Request overlaps',
+                                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                                $conflict = true;
+                                break;
+                            }
+                        }
+                        if ($conflict) {
+                            break;
+                        }
+                    }
+
+                    if ($conflict) {
+                        if ($action == RM_ACT_MANUAL_IF_CONFLICTS) {
+                            //sendITipReply(RM_ITIP_TENTATIVE);
+                            Horde::logMessage('Conflict detected; Passing mail through',
+                                              __FILE__, __LINE__, PEAR_LOG_INFO);
+                            return true;
+                        } else if ($action == RM_ACT_REJECT_IF_CONFLICTS) {
+                            Horde::logMessage('Conflict detected; rejecting',
+                                              __FILE__, __LINE__, PEAR_LOG_INFO);
+                            $this->sendITipReply($cn, $id, $itip, RM_ITIP_DECLINE,
+                                                 $organiser, $uid, $is_update);
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            if (is_a($imap_error, 'PEAR_Error')) {
+                Horde::logMessage('Could not access users calendar; rejecting',
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+                $this->sendITipReply($cn, $id, $itip, RM_ITIP_DECLINE,
+                                     $organiser, $uid, $is_update);
+                return false;
+            }
+
+            // At this point there was either no conflict or RM_ACT_ALWAYS_ACCEPT
+            // was specified; either way we add the new event & send an 'ACCEPT'
+            // iTip reply
+
+            Horde::logMessage(sprintf('Adding event %s', $uid),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+
+            if (!empty($conf['kolab']['filter']['simple_locks'])) {
+                if (!empty($conf['kolab']['filter']['simple_locks_timeout'])) {
+                    $timeout = $conf['kolab']['filter']['simple_locks_timeout'];
+                } else {
+                    $timeout = 60;
+                }
+                if (!empty($conf['kolab']['filter']['simple_locks_dir'])) {
+                    $lockdir = $conf['kolab']['filter']['simple_locks_dir'];
+                } else {
+                    $lockdir = Horde::getTempDir() . '/Kolab_Filter_locks';
+                    if (!is_dir($lockdir)) {
+                        mkdir($lockdir, 0700);
+                    }
+                }
+                if (is_dir($lockdir)) {
+                    $lockfile = $lockdir . '/' . $resource . '.lock';
+                    $counter = 0;
+                    while ($counter < $timeout && @file_get_contents($lockfile) == 'LOCKED') {
+                        sleep(1);
+                        $counter++;
+                    }
+                    if ($counter == $timeout) {
+                        Horde::logMessage(sprintf('Lock timeout of %s seconds exceeded. Rejecting invitation.', $timeout),
+                                          __FILE__, __LINE__, PEAR_LOG_ERR);
+                        $this->sendITipReply($cn, $id, $itip, RM_ITIP_DECLINE,
+                                             $organiser, $uid, $is_update);
+                        return false;
+                    }
+                    $result = file_put_contents($lockfile, 'LOCKED');
+                    if ($result === false) {
+                        Horde::logMessage(sprintf('Failed creating lock file %s.', $lockfile),
+                                          __FILE__, __LINE__, PEAR_LOG_ERR);
+                    } else {
+                        $this->lockfile = $lockfile;
+                    }
+                } else {
+                    Horde::logMessage(sprintf('The lock directory %s is missing. Disabled locking.', $lockdir),
+                                      __FILE__, __LINE__, PEAR_LOG_ERR);
+                }
+            }
+
+            $result = $data->save($object, $old_uid);
+            if (is_a($result, 'PEAR_Error')) {
+                $result->code = OUT_LOG | EX_UNAVAILABLE;
+                return $result;
+            }
+
+            // Update our status within the iTip request and send the reply
+            $itip->setAttribute('STATUS', 'CONFIRMED', array(), false);
+            $attendees = $itip->getAttribute('ATTENDEE');
+            if (!is_array($attendees)) {
+                $attendees = array($attendees);
+            }
+            $attparams = $itip->getAttribute('ATTENDEE', true);
+            foreach ($attendees as $i => $attendee) {
+                $attendee = preg_replace('/^mailto:\s*/i', '', $attendee);
+                if ($attendee != $resource) {
+                    continue;
+                }
+
+                $attparams[$i]['PARTSTAT'] = 'ACCEPTED';
+                if (array_key_exists('RSVP', $attparams[$i])) {
+                    unset($attparams[$i]['RSVP']);
+                }
+            }
+
+            // Re-add all the attendees to the event, using our updates status info
+            $firstatt = array_pop($attendees);
+            $firstattparams = array_pop($attparams);
+            $itip->setAttribute('ATTENDEE', $firstatt, $firstattparams, false);
+            foreach ($attendees as $i => $attendee) {
+                $itip->setAttribute('ATTENDEE', $attendee, $attparams[$i]);
+            }
+
+            if ($outofperiod) {
+                $this->sendITipReply($cn, $resource, $itip, RM_ITIP_TENTATIVE,
+                                     $organiser, $uid, $is_update);
+                Horde::logMessage('No freebusy information available',
+                                  __FILE__, __LINE__, PEAR_LOG_NOTICE);
+            } else {
+                $this->sendITipReply($cn, $resource, $itip, RM_ITIP_ACCEPT,
+                                     $organiser, $uid, $is_update);
+            }
+            return false;
+
+        case 'CANCEL':
+            Horde::logMessage(sprintf('Removing event %s', $uid),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+
+            if (is_a($imap_error, 'PEAR_Error')) {
+                $body = sprintf(_("Unable to access %s's calendar:"), $resource) . "\n\n" . $summary;
+                $subject = sprintf(_("Error processing \"%s\""), $summary);
+            } else if (!$data->objectUidExists($uid)) {
+                Horde::logMessage(sprintf('Canceled event %s is not present in %s\'s calendar',
+                                          $uid, $resource),
+                                  __FILE__, __LINE__, PEAR_LOG_WARNING);
+                $body = sprintf(_("The following event that was canceled is not present in %s's calendar:"), $resource) . "\n\n" . $summary;
+                $subject = sprintf(_("Error processing \"%s\""), $summary);
+            } else {
+                /**
+                 * Delete the messages from IMAP
+                 * Delete any old events that we updated
+                 */
+                Horde::logMessage(sprintf('Deleting %s because of cancel',
+                                          $uid),
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                $result = $data->delete($uid);
+                if (is_a($result, 'PEAR_Error')) {
+                    Horde::logMessage(sprintf('Deleting %s failed with %s',
+                                              $uid, $result->getMessage()),
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                }
+
+                $body = _("The following event has been successfully removed:") . "\n\n" . $summary;
+                $subject = sprintf(_("%s has been cancelled"), $summary);
+            }
+
+            Horde::logMessage(sprintf('Sending confirmation of cancelation to %s', $organiser),
+                              __FILE__, __LINE__, PEAR_LOG_WARNING);
+
+            $body = new MIME_Part('text/plain', Horde_String::wrap($body, 76, "\n", 'utf-8'), 'utf-8');
+            $mime = &MIME_Message::convertMimePart($body);
+            $mime->setTransferEncoding('quoted-printable');
+            $mime->transferEncodeContents();
+
+            // Build the reply headers.
+            $msg_headers = new MIME_Headers();
+            $msg_headers->addHeader('Date', date('r'));
+            $msg_headers->addHeader('From', $resource);
+            $msg_headers->addHeader('To', $organiser);
+            $msg_headers->addHeader('Subject', $subject);
+            $msg_headers->addMIMEHeaders($mime);
+
+            $result = $this->transportReply($resource, MIME::encodeAddress($organiser),
+                                            $msg_headers->toString() . '\r\n\r\n' . $mime->toString());
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            Horde::logMessage('Successfully sent cancellation reply',
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+
+            return false;;
+
+        default:
+            // We either don't currently handle these iTip methods, or they do not
+            // apply to what we're trying to accomplish here
+            Horde::logMessage(sprintf('Ignoring %s method and passing message through to %s',
+                                      $method, $resource),
+                              __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+    }
+
+    /**
+     * Helper function to clean up after handling an invitation
+     *
+     * @return NULL
+     */
+    function cleanup()
+    {
+        if (!empty($this->lockfile)) {
+            @unlink($this->lockfile);
+            if (file_exists($this->lockfile)) {
+                Horde::logMessage(sprintf('Failed removing the lockfile %s.', $lockfile),
+                                  __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+            $this->lockfile = null;
+        }
+    }
+
+    /**
+     * Send an automated reply.
+     *
+     * @param string  $cn                     Common name to be used in the iTip
+     *                                        response.
+     * @param string  $resource               Resource we send the reply for.
+     * @param string  $Horde_iCalendar_vevent The iTip information.
+     * @param int     $type                   Type of response.
+     * @param string  $organiser              The event organiser.
+     * @param string  $uid                    The UID of the event.
+     * @param boolean $is_update              Is this an event update?
+     */
+    function sendITipReply($cn, $resource, $itip, $type = RM_ITIP_ACCEPT,
+                           $organiser, $uid, $is_update)
+    {
+        Horde::logMessage(sprintf('sendITipReply(%s, %s, %s, %s)',
+                                  $cn, $resource, get_class($itip), $type),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // Build the reply.
+        $vCal = new Horde_iCalendar();
+        $vCal->setAttribute('PRODID', '-//kolab.org//NONSGML Kolab Server 2//EN');
+        $vCal->setAttribute('METHOD', 'REPLY');
+
+        $summary = _('No summary available');
+
+        $itip_reply =& Horde_iCalendar::newComponent('VEVENT', $vCal);
+        $itip_reply->setAttribute('UID', $uid);
+        if (!is_a($itip->getAttribute('SUMMARY'), 'PEAR_error')) {
+            $itip_reply->setAttribute('SUMMARY', $itip->getAttribute('SUMMARY'));
+            $summary = $itip->getAttribute('SUMMARY');
+        }
+        if (!is_a($itip->getAttribute('DESCRIPTION'), 'PEAR_error')) {
+            $itip_reply->setAttribute('DESCRIPTION', $itip->getAttribute('DESCRIPTION'));
+        }
+        if (!is_a($itip->getAttribute('LOCATION'), 'PEAR_error')) {
+            $itip_reply->setAttribute('LOCATION', $itip->getAttribute('LOCATION'));
+        }
+        $itip_reply->setAttribute('DTSTART', $itip->getAttribute('DTSTART'), array_pop($itip->getAttribute('DTSTART', true)));
+        if (!is_a($itip->getAttribute('DTEND'), 'PEAR_error')) {
+            $itip_reply->setAttribute('DTEND', $itip->getAttribute('DTEND'), array_pop($itip->getAttribute('DTEND', true)));
+        } else {
+            $itip_reply->setAttribute('DURATION', $itip->getAttribute('DURATION'), array_pop($itip->getAttribute('DURATION', true)));
+        }
+        if (!is_a($itip->getAttribute('SEQUENCE'), 'PEAR_error')) {
+            $itip_reply->setAttribute('SEQUENCE', $itip->getAttribute('SEQUENCE'));
+        } else {
+            $itip_reply->setAttribute('SEQUENCE', 0);
+        }
+        $itip_reply->setAttribute('ORGANIZER', $itip->getAttribute('ORGANIZER'), array_pop($itip->getAttribute('ORGANIZER', true)));
+
+        // Let's try and remove this code and just create
+        // the ATTENDEE stuff in the reply from scratch
+        //     $attendees = $itip->getAttribute( 'ATTENDEE' );
+        //     if( !is_array( $attendees ) ) {
+        //       $attendees = array( $attendees );
+        //     }
+        //     $params = $itip->getAttribute( 'ATTENDEE', true );
+        //     for( $i = 0; $i < count($attendees); $i++ ) {
+        //       $attendee = preg_replace('/^mailto:\s*/i', '', $attendees[$i]);
+        //       if ($attendee != $resource) {
+        //           continue;
+        //       }
+        //       $params = $params[$i];
+        //       break;
+        //     }
+
+        $params = array();
+        $params['CN'] = $cn;
+        switch ($type) {
+        case RM_ITIP_DECLINE:
+            Horde::logMessage(sprintf('Sending DECLINE iTip reply to %s',
+                                      $organiser),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $message = $is_update
+                ? sprintf(_("%s has declined the update to the following event:"), $resource) . "\n\n" . $summary
+                : sprintf(_("%s has declined the invitation to the following event:"), $resource) . "\n\n" . $summary;
+            $subject = _("Declined: ") . $summary;
+            $params['PARTSTAT'] = 'DECLINED';
+            break;
+
+        case RM_ITIP_ACCEPT:
+            Horde::logMessage(sprintf('Sending ACCEPT iTip reply to %s', $organiser),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $message = $is_update
+                ? sprintf(_("%s has accepted the update to the following event:"), $resource) . "\n\n" . $summary
+                : sprintf(_("%s has accepted the invitation to the following event:"), $resource) . "\n\n" . $summary;
+            $subject = _("Accepted: ") . $summary;
+            $params['PARTSTAT'] = 'ACCEPTED';
+            break;
+
+        case RM_ITIP_TENTATIVE:
+            Horde::logMessage(sprintf('Sending TENTATIVE iTip reply to %s', $organiser),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $message = $is_update
+                ? sprintf(_("%s has tentatively accepted the update to the following event:"), $resource) . "\n\n" . $summary
+                : sprintf(_("%s has tentatively accepted the invitation to the following event:"), $resource) . "\n\n" . $summary;
+            $subject = _("Tentative: ") . $summary;
+            $params['PARTSTAT'] = 'TENTATIVE';
+            break;
+
+        default:
+            Horde::logMessage(sprintf('Unknown iTip method (%s passed to sendITipReply())', $type),
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        $itip_reply->setAttribute('ATTENDEE', 'MAILTO:' . $resource, $params);
+        $vCal->addComponent($itip_reply);
+
+        $ics = new MIME_Part('text/calendar', $vCal->exportvCalendar(), 'UTF-8' );
+        //$ics->setName('event-reply.ics');
+        $ics->setContentTypeParameter('method', 'REPLY');
+
+        //$mime->addPart($body);
+        //$mime->addPart($ics);
+        // The following was ::convertMimePart($mime). This was removed so that we
+        // send out single-part MIME replies that have the iTip file as the body,
+        // with the correct mime-type header set, etc. The reason we want to do this
+        // is so that Outlook interprets the messages as it does Outlook-generated
+        // responses, i.e. double-clicking a reply will automatically update your
+        // meetings, showing different status icons in the UI, etc.
+        $mime = &MIME_Message::convertMimePart($ics);
+        $mime->setCharset('UTF-8');
+        $mime->setTransferEncoding('quoted-printable');
+        $mime->transferEncodeContents();
+
+        // Build the reply headers.
+        $msg_headers = new MIME_Headers();
+        $msg_headers->addHeader('Date', date('r'));
+        $msg_headers->addHeader('From', "$cn <$resource>");
+        $msg_headers->addHeader('To', $organiser);
+        $msg_headers->addHeader('Subject', $subject);
+        $msg_headers->addMIMEHeaders($mime);
+
+        $result = $this->transportReply($resource, MIME::encodeAddress($organiser),
+                                        $msg_headers->toString() . '\r\n\r\n' . $mime->toString());
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        Horde::logMessage('Successfully sent iTip reply',
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+    }
+
+
+    function transportReply($sender, $recipients, $data)
+    {
+        global $conf;
+
+        if (isset($conf['kolab']['filter']['itipreply'])) {
+            $driver = $conf['kolab']['filter']['itipreply']['driver'];
+            $host   = $conf['kolab']['filter']['itipreply']['params']['host'];
+            $port   = $conf['kolab']['filter']['itipreply']['params']['port'];
+        } else {
+            $driver = 'smtp';
+            $host   = 'localhost';
+            $port   = 25;
+        }
+
+        $transport = &Horde_Kolab_Filter_Transport::factory($driver,
+                                                            array('host' => $host,
+                                                                  'port' => $port));
+
+        $result = $transport->start($sender, $recipients);
+        if (is_a($result, 'PEAR_Error')) {
+            return PEAR::raiseError('Unable to send iTip reply: ' . $result->getMessage(),
+                                    OUT_LOG | EX_TEMPFAIL);
+        }
+
+        $result = $transport->data($data);
+        if (is_a($result, 'PEAR_Error')) {
+            return PEAR::raiseError('Unable to send iTip reply: ' . $result->getMessage(),
+                                    OUT_LOG | EX_TEMPFAIL);
+        }
+
+        $result = $transport->end();
+        if (is_a($result, 'PEAR_Error')) {
+            return PEAR::raiseError('Unable to send iTip reply: ' . $result->getMessage(),
+                                    OUT_LOG | EX_TEMPFAIL);
+        }
+    }
+
+    /**
+     * Clear information from a date array.
+     *
+     * @param array $ical_date  The array to clear.
+     *
+     * @return array The cleaned array.
+     */
+    function cleanArray($ical_date)
+    {
+        if (!array_key_exists('hour', $ical_date)) {
+            $temp['DATE'] = '1';
+        }
+        $temp['hour']   = array_key_exists('hour', $ical_date) ? $ical_date['hour'] :  '00';
+        $temp['minute']   = array_key_exists('minute', $ical_date) ? $ical_date['minute'] :  '00';
+        $temp['second']   = array_key_exists('second', $ical_date) ? $ical_date['second'] :  '00';
+        $temp['year']   = array_key_exists('year', $ical_date) ? $ical_date['year'] :  '0000';
+        $temp['month']   = array_key_exists('month', $ical_date) ? $ical_date['month'] :  '00';
+        $temp['mday']   = array_key_exists('mday', $ical_date) ? $ical_date['mday'] :  '00';
+        $temp['zone']   = array_key_exists('zone', $ical_date) ? $ical_date['zone'] :  'UTC';
+
+        return $temp;
+    }
+
+    /**
+     * Conveert iCal dates to Kolab format.
+     *
+     * An all day event must have a dd--mm-yyyy notation and not a
+     * yyyy-dd-mmT00:00:00z notation Otherwise the event is shown as a
+     * 2-day event --> do not try to convert everything to epoch first
+     *
+     * @param array  $ical_date  The array to convert.
+     * @param string $type       The type of the date to convert.
+     *
+     * @return string The converted date.
+     */
+    function iCalDate2Kolab($ical_date, $type= ' ')
+    {
+        Horde::logMessage(sprintf('Converting to kolab format %s',
+                                  print_r($ical_date, true)),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // $ical_date should be a timestamp
+        if (is_array($ical_date)) {
+            // going to create date again
+            $temp = $this->cleanArray($ical_date);
+            if (array_key_exists('DATE', $temp)) {
+                if ($type == 'ENDDATE') {
+                    // substract a day (86400 seconds) using epochs to take number of days per month into account
+                    $epoch= $this->convert2epoch($temp) - 86400;
+                    $date = gmstrftime('%Y-%m-%d', $epoch);
+                } else {
+                    $date= sprintf('%04d-%02d-%02d', $temp['year'], $temp['month'], $temp['mday']);
+                }
+            } else {
+                $time = sprintf('%02d:%02d:%02d', $temp['hour'], $temp['minute'], $temp['second']);
+                if ($temp['zone'] == 'UTC') {
+                    $time .= 'Z';
+                }
+                $date = sprintf('%04d-%02d-%02d', $temp['year'], $temp['month'], $temp['mday']) . 'T' . $time;
+            }
+        }  else {
+            $date = gmstrftime('%Y-%m-%dT%H:%M:%SZ', $ical_date);
+        }
+        Horde::logMessage(sprintf('To <%s>', $date),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $date;
+    }
+
+    /**
+     * Convert a date to an epoch.
+     *
+     * @param array  $values  The array to convert.
+     *
+     * @return int Time.
+     */
+    function convert2epoch($values)
+    {
+        Horde::logMessage(sprintf('Converting to epoch %s',
+                                  print_r($values, true)),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        if (is_array($values)) {
+            $temp = $this->cleanArray($values);
+            $epoch = gmmktime($temp['hour'], $temp['minute'], $temp['second'],
+                              $temp['month'], $temp['mday'], $temp['year']);
+        } else {
+            $epoch = $values;
+        }
+
+        Horde::logMessage(sprintf('Converted <%s>', $epoch),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $epoch;
+    }
+}
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy.php b/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy.php
new file mode 100644 (file)
index 0000000..4a2bb41
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Provides methods to retrieve free/busy data for resources.
+ *
+ * PHP version 5
+ * 
+ * @todo Merge this class with Kolab_FreeBusy and Kronolith_FreeBusy into a
+ *       single Horde_Freebusy handler.
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+
+/**
+ * The Autoloader allows us to omit "require/include" statements.
+ */
+require_once 'Horde/Autoloader.php';
+
+/**
+ * Retrieves free/busy data for an email address.
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL>=2.1). If you
+ * did not receive this file,
+ * see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+class Horde_Kolab_Resource_Freebusy
+{
+    /**
+     * Singleton instances.
+     *
+     * @var array
+     */
+    static protected $_instances = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $params A hash containing any additional configuration or
+     *                      connection parameters a subclass might need.
+     */
+    protected function __construct($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Kolab_Resource_Getfreebusy instance
+     * based on $driver.
+     *
+     * @param mixed $driver The type of concrete
+     *                      Horde_Kolab_Resource_Getfreebusy subclass to
+     *                      return.
+     * @param array $params A hash containing any additional configuration or
+     *                      connection parameters a subclass might need.
+     *
+     * @return Horde_Kolab_Resource_Getfreebusy The newly created concrete
+     *                                          Horde_Kolab_Resource_Getfreebusy
+     *                                          instance, or false an error.
+     */
+    static public function factory($driver, $params = array())
+    {
+        $driver = basename($driver);
+        $class  = ($driver == 'none')
+            ? 'Horde_Kolab_Resource_Freebusy'
+            : 'Horde_Kolab_Resource_Freebusy_' . ucfirst($driver);
+
+        if (!class_exists($class)) {
+            $class = 'Horde_Kolab_Resource_Freebusy';
+        }
+
+        return new $class($params);
+    }
+
+    /**
+     * Attempts to return a reference to a concrete
+     * Horde_Kolab_Resource_Getfreebusy instance based on $driver.
+     *
+     * It will only create a new instance if no Horde_Kolab_Resource_Getfreebusy
+     * instance with the same parameters currently exists.
+     *
+     * This method must be invoked as:
+     * <code>$var = Horde_Kolab_Resource_Getfreebusy::singleton();</code>
+     *
+     * @param mixed $driver The type of concrete
+     *                      Horde_Kolab_Resource_Getfreebusy subclass to
+     *                      return.
+     * @param array $params A hash containing any additional configuration or
+     *                      connection parameters a subclass might need.
+     *
+     * @return Horde_Token The concrete Horde_Kolab_Resource_Getfreebusy
+     *                      reference, or false on error.
+     */
+    static public function singleton($driver = null, $params = array())
+    {
+        global $conf;
+
+        if (empty($driver)) {
+            $driver = $conf['freebusy']['driver'];
+        }
+
+        ksort($params);
+        $sig = hash('md5', serialize(array($driver, $params)));
+
+        if (!isset(self::$_instances[$sig])) {
+            self::$_instances[$sig] = Horde_Kolab_Resource_Freebusy::factory($driver,
+                                                                             $params);
+        }
+
+        return self::$_instances[$sig];
+    }
+
+    /**
+     * Retrieve Free/Busy URL for the specified resource id.
+     *
+     * @param string $resource The id of the resource (usually a mail address).
+     *
+     * @return string The Free/Busy URL for that resource.
+     */
+    protected function getUrl($resource)
+    {
+        return '';
+    }
+
+    /**
+     * Retrieve Free/Busy data for the specified resource.
+     *
+     * @param string $resource Fetch the Free/Busy data for this resource
+     *                         (usually a mail address).
+     *
+     * @return Horde_iCalendar_vfreebusy The Free/Busy data.
+     */
+    public function get($resource)
+    {
+        /* Return an empty VFB object. */
+        $vCal = new Horde_iCalendar();
+        $vFb = Horde_iCalendar::newComponent('vfreebusy', $vCal);
+        $vFb->setAttribute('ORGANIZER', $resource);
+
+        return $vFb;
+
+    }
+}
\ No newline at end of file
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Kolab.php b/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Kolab.php
new file mode 100644 (file)
index 0000000..ed6074e
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Provides methods to retrieve free/busy data for resources on a Kolab server.
+ *
+ * PHP version 5
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+
+/**
+ * Retrieves free/busy data for an email address on a Kolab server.
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL>=2.1). If you
+ * did not receive this file,
+ * see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+class Horde_Kolab_Resource_Freebusy_Kolab extends Horde_Kolab_Resource_Freebusy
+{
+    /**
+     * Retrieve Free/Busy URL for the specified resource id.
+     *
+     * @param string $resource The id of the resource (usually a mail address).
+     *
+     * @return string The Free/Busy URL for that resource.
+     */
+    static protected function getUrl($resource)
+    {
+        $server = Horde_Kolab_Server::singleton();
+        $uid    = $server->uidForIdOrMailOrAlias($resource);
+        $result = $server->fetch($uid)->getServer('freebusy');
+        return sprintf('%s/%s.xfb', $result, $resource);
+    }
+
+    /**
+     * Retrieve Free/Busy data for the specified resource.
+     *
+     * @param string $resource Fetch the Free/Busy data for this resource.
+     *
+     * @return Horde_iCalendar_vfreebusy The Free/Busy data.
+     */
+    public function get($resource)
+    {
+        global $conf;
+
+        $url = self::getUrl($resource);
+
+        Horde::logMessage(sprintf('Freebusy URL for resource %s is %s',
+                                  $resource, $url),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        list($user, $domain) = explode('@', $resource);
+        if (empty($domain)) {
+            $domain = $conf['kolab']['filter']['email_domain'];
+        }
+
+        /**
+         * This section matches Kronolith_Freebusy and should be merged with it
+         * again in a single Horde_Freebusy module.
+         */
+        $options = array(
+            'method'         => 'GET',
+            'timeout'        => 5,
+            'allowRedirects' => true
+        );
+
+        if (!empty($conf['http']['proxy']['proxy_host'])) {
+            $options = array_merge($options, $conf['http']['proxy']);
+        }
+
+        $http = new HTTP_Request($url, $options);
+        $http->setBasicAuth($conf['kolab']['filter']['calendar_id'] . '@' . $domain,
+                            $conf['kolab']['filter']['calendar_pass']);
+        @$http->sendRequest();
+        if ($http->getResponseCode() != 200) {
+            throw new Horde_Kolab_Filter_Exception(sprintf('Unable to retrieve free/busy information for %s',
+                                                           $resource),
+                                                   Horde_Kolab_Filter_Exception::NO_FREEBUSY);
+        }
+        $vfb_text = $http->getResponseBody();
+
+        // Detect the charset of the iCalendar data.
+        $contentType = $http->getResponseHeader('Content-Type');
+        if ($contentType && strpos($contentType, ';') !== false) {
+            list(,$charset,) = explode(';', $contentType);
+            $charset         = trim(str_replace('charset=', '', $charset));
+        } else {
+            $charset = 'UTF-8';
+        }
+
+        $iCal = new Horde_iCalendar;
+        $iCal->parsevCalendar($vfb_text, 'VCALENDAR', $charset);
+
+        $vfb = &$iCal->findComponent('VFREEBUSY');
+
+        if ($vfb === false) {
+            throw new Horde_Kolab_Filter_Exception(sprintf('Invalid or no free/busy information available for %s',
+                                                           $resource),
+                                                   Horde_Kolab_Filter_Exception::NO_FREEBUSY);
+        }
+        $vfb->simplify();
+
+        return $vfb;
+    }
+}
\ No newline at end of file
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Mock.php b/framework/Kolab_Filter/lib/Horde/Kolab/Resource/Freebusy/Mock.php
new file mode 100644 (file)
index 0000000..5137353
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Provides mockup methods to retrieve free/busy data for resources.
+ *
+ * PHP version 5
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+
+/**
+ * Retrieves free/busy mockup data.
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL>=2.1). If you
+ * did not receive this file,
+ * see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Filter
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+class Horde_Kolab_Resource_Freebusy_Mock extends Horde_Kolab_Resource_Freebusy
+{
+    /**
+     * Retrieve Free/Busy URL for the specified resource id.
+     *
+     * @param string $resource The id of the resource (usually a mail address).
+     *
+     * @return string The Free/Busy URL for that resource.
+     */
+    protected function getUrl($resource)
+    {
+        return '';
+    }
+
+    /**
+     * Retrieve Free/Busy data for the specified resource.
+     *
+     * @param string $resource Fetch the Free/Busy data for this resource
+     *                         (usually a mail address).
+     *
+     * @return Horde_iCalendar_vfreebusy The Free/Busy data.
+     */
+    public function get($resource)
+    {
+        return parent::get($resource);
+    }
+}
\ No newline at end of file
diff --git a/framework/Kolab_Filter/lib/Horde/Kolab/Test/Filter.php b/framework/Kolab_Filter/lib/Horde/Kolab/Test/Filter.php
new file mode 100644 (file)
index 0000000..2bdab1b
--- /dev/null
@@ -0,0 +1,355 @@
+<?php
+/**
+ * Base for PHPUnit scenarios.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Test/Filter.php,v 1.9 2009/06/09 23:23:37 slusarz Exp $
+ *
+ * PHP version 5
+ *
+ * @category Kolab
+ * @package  Kolab_Test
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+
+/**
+ *  We need the unit test framework
+ */
+require_once 'Horde/Kolab/Test/Storage.php';
+
+/**
+ * Base for PHPUnit scenarios.
+ *
+ * $Horde: framework/Kolab_Filter/lib/Horde/Kolab/Test/Filter.php,v 1.9 2009/06/09 23:23:37 slusarz Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Test
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+class Horde_Kolab_Test_Filter extends Horde_Kolab_Test_Storage
+{
+    /**
+     * Handle a "given" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runGiven(&$world, $action, $arguments)
+    {
+        switch($action) {
+        case 'a set of Kolab test servers':
+            $world['servers'] = array();
+            $world['servers']['server'] = $this->prepareEmptyKolabServer();
+            $world['servers']['freebusy'] = $this->prepareEmptyKolabServer();
+            break;
+        case 'a set of test accounts':
+            foreach ($world['servers'] as $server) {
+                $this->prepareUsers($server['server']);
+            }
+        case 'a set of events where the test resource is busy':
+            foreach ($world['servers'] as $server) {
+                $this->prepareUsers($server);
+            }
+        default:
+            return parent::runGiven($world, $action, $arguments);
+        }
+    }
+
+    /**
+     * Handle a "when" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runWhen(&$world, $action, $arguments)
+    {
+        switch($action) {
+        case 'inviting the resource':
+        default:
+            return parent::runWhen($world, $action, $arguments);
+        }
+    }
+
+    /**
+     * Handle a "then" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runThen(&$world, $action, $arguments)
+    {
+        switch($action) {
+        case 'the invitation is being rejected':
+        case 'the invitation is being accepted':
+        case 'the resource contains the event':
+        case 'the resource is busy during':
+        case 'the response contains':
+        default:
+            return parent::runThen($world, $action, $arguments);
+        }
+    }
+
+    /**
+     * Fill a Kolab Server with test users.
+     *
+     * @param Kolab_Server &$server The server to populate.
+     *
+     * @return Horde_Kolab_Server The empty server.
+     */
+    public function prepareUsers(&$server)
+    {
+        parent::prepareUsers(&$server);
+        $result = $server->add($this->provideFilterUserOne());
+        $this->assertNoError($result);
+        $result = $server->add($this->provideFilterUserTwo());
+        $this->assertNoError($result);
+        $result = $server->add($this->provideFilterUserThree());
+        $this->assertNoError($result);
+        $result = $server->add($this->provideFilterCalendarUser());
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Prepare the configuration.
+     *
+     * @return NULL
+     */
+    public function prepareConfiguration()
+    {
+        $fh = fopen(HORDE_BASE . '/config/conf.php', 'w');
+        $data = <<<EOD
+\$conf['use_ssl'] = 2;
+\$conf['server']['name'] = \$_SERVER['SERVER_NAME'];
+\$conf['server']['port'] = \$_SERVER['SERVER_PORT'];
+\$conf['debug_level'] = E_ALL;
+\$conf['umask'] = 077;
+\$conf['compress_pages'] = true;
+\$conf['menu']['always'] = false;
+\$conf['portal']['fixed_blocks'] = array();
+\$conf['imsp']['enabled'] = false;
+
+/** Additional config variables required for a clean Horde setup */
+\$conf['session']['use_only_cookies'] = false;
+\$conf['session']['timeout'] = 0;
+\$conf['cookie']['path'] = '/';
+\$conf['cookie']['domain'] = \$_SERVER['SERVER_NAME'];
+\$conf['use_ssl'] = false;
+\$conf['session']['cache_limiter'] = 'nocache';
+\$conf['session']['name'] = 'Horde';
+\$conf['log']['enabled'] = false;
+\$conf['prefs']['driver'] = 'session';
+\$conf['auth']['driver'] = 'kolab';
+\$conf['share']['driver'] = 'kolab';
+\$conf['debug_level'] = E_ALL;
+
+/** Make the share driver happy */
+\$conf['kolab']['enabled'] = true;
+
+/** Ensure we still use the LDAP test driver */
+\$conf['kolab']['server']['driver'] = 'test';
+
+/** Ensure that we do not trigger on folder update */
+\$conf['kolab']['no_triggering'] = true;
+
+/** Storage location for the free/busy system */
+\$conf['fb']['cache_dir']             = '/tmp';
+\$conf['kolab']['freebusy']['server'] = 'https://fb.example.org/freebusy';
+
+/** Setup the virtual file system for Kolab */
+\$conf['vfs']['params']['all_folders'] = true;
+\$conf['vfs']['type'] = 'kolab';
+
+\$conf['kolab']['imap']['server'] = 'localhost';
+\$conf['kolab']['imap']['port']   = 0;
+\$conf['kolab']['imap']['allow_special_users'] = true;
+\$conf['kolab']['filter']['reject_forged_from_header'] = false;
+\$conf['kolab']['filter']['email_domain'] = 'example.org';
+\$conf['kolab']['filter']['privileged_networks'] = '127.0.0.1,192.168.0.0/16';
+\$conf['kolab']['filter']['verify_from_header'] = true;
+\$conf['kolab']['filter']['calendar_id'] = 'calendar';
+\$conf['kolab']['filter']['calendar_pass'] = 'calendar';
+\$conf['kolab']['filter']['lmtp_host'] = 'imap.example.org';
+\$conf['kolab']['filter']['simple_locks'] = true;
+\$conf['kolab']['filter']['simple_locks_timeout'] = 3;
+
+\$conf['kolab']['filter']['itipreply']['driver'] = 'echo';
+\$conf['kolab']['filter']['itipreply']['params']['host'] = 'localhsot';
+\$conf['kolab']['filter']['itipreply']['params']['port'] = 25;
+
+\$conf['freebusy']['driver'] = 'Mock';
+EOD;
+        fwrite($fh, "<?php\n" . $data);
+        fclose($fh);
+    }
+
+    /**
+     * Return a test user.
+     *
+     * @return array The test user.
+     */
+    public function provideFilterUserOne()
+    {
+        return array(
+            'type' => 'Horde_Kolab_Server_Object_Kolab_User',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_GIVENNAME => 'Me',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SN => 'Me',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL => 'me@example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SID => 'me',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_USERPASSWORD => 'me',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_HOMESERVER => 'home.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IMAPHOST => 'imap.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_FREEBUSYHOST => 'https://fb.example.org/freebusy',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IPOLICY => array('ACT_REJECT_IF_CONFLICTS'),
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_ALIAS => array('me.me@example.org', 'MEME@example.org'),
+        );
+    }
+
+    /**
+     * Return a test user.
+     *
+     * @return array The test user.
+     */
+    public function provideFilterUserTwo()
+    {
+        return array(
+            'type' => 'Horde_Kolab_Server_Object_Kolab_User',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_GIVENNAME => 'You',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SN => 'You',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL => 'you@example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SID => 'you',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_USERPASSWORD => 'you',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_HOMESERVER => 'home.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IMAPHOST => 'home.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_FREEBUSYHOST => 'https://fb.example.org/freebusy',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_ALIAS => array('you.you@example.org'),
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_DELEGATE => array('wrobel@example.org'),
+        );
+    }
+
+    /**
+     * Return a test user.
+     *
+     * @return array The test user.
+     */
+    public function provideFilterUserThree()
+    {
+        return array(
+            'type' => 'Horde_Kolab_Server_Object_Kolab_User',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_GIVENNAME => 'Else',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SN => 'Else',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL => 'else@example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SID => 'else',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_USERPASSWORD => 'you',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_HOMESERVER => 'imap.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IMAPHOST => 'imap.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_FREEBUSYHOST => 'https://fb.example.org/freebusy',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_DELEGATE => array('me@example.org'),
+        );
+    }
+
+    /**
+     * Return the calendar user.
+     *
+     * @return array The calendar user.
+     */
+    public function provideFilterCalendarUser()
+    {
+        return array(
+            'type' => 'Horde_Kolab_Server_Object_Kolab_User',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN => 'calendar',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_GIVENNAME => '',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SN => 'calendar',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL => 'calendar@example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_SID => 'calendar@home.example.com',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_USERPASSWORD => 'calendar',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_HOMESERVER => 'home.example.org',
+            Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_IMAPHOST => 'imap.example.org',
+        );
+    }
+
+    public function sendFixture($infile, $outfile, $user, $client, $from, $to,
+                                $host, $params = array())
+    {
+        $_SERVER['argv'] = array($_SERVER['argv'][0],
+                                 '--sender=' . $from,
+                                 '--recipient=' . $to,
+                                 '--user=' . $user,
+                                 '--host=' . $host,
+                                 '--client=' . $client);
+
+        $in = file_get_contents($infile, 'r');
+
+        $tmpfile = Horde_Util::getTempFile('KolabFilterTest');
+        $tmpfh = @fopen($tmpfile, 'w');
+        if (empty($params['unmodified_content'])) {
+            @fwrite($tmpfh, sprintf($in, $from, $to));
+        } else {
+            @fwrite($tmpfh, $in);
+        }
+        @fclose($tmpfh);
+
+        $inh = @fopen($tmpfile, 'r');
+
+        /* Setup the class */
+        if (empty($params['incoming'])) {
+            require_once 'Horde/Kolab/Filter/Content.php';
+            $parser = &new Horde_Kolab_Filter_Content();
+        } else {
+            require_once 'Horde/Kolab/Filter/Incoming.php';
+            $parser = &new Horde_Kolab_Filter_Incoming();
+        }
+
+        ob_start();
+
+        /* Parse the mail */
+        $result = $parser->parse($inh, 'echo');
+        if (empty($params['error'])) {
+            $this->assertNoError($result);
+            $this->assertTrue(empty($result));
+
+            $output = ob_get_contents();
+            ob_end_clean();
+
+            $out = file_get_contents($outfile);
+            $replace = array(
+                '/^Received:.*$/m' => '',
+                '/^Date:.*$/m' => '',
+                '/DTSTAMP:.*$/m' => '',
+                '/^--+=.*$/m' => '----',
+                '/^Message-ID.*$/m' => '----',
+                '/boundary=.*$/m' => '----',
+                '/\s/' => '',
+            );
+            foreach ($replace as $pattern => $replacement) {
+                $output = preg_replace($pattern, $replacement, $output);
+                $out    = preg_replace($pattern, $replacement, $out);
+            }
+
+            if (empty($params['unmodified_content'])) {
+                $this->assertEquals(sprintf($out, $from, $to), $output);
+            } else {
+                $this->assertEquals($out, $output);
+            }
+        } else {
+            $this->assertError($result, $params['error']);
+        }
+
+    }
+}
diff --git a/framework/Kolab_Filter/locale/de_DE/LC_MESSAGES/Kolab_Filter.mo b/framework/Kolab_Filter/locale/de_DE/LC_MESSAGES/Kolab_Filter.mo
new file mode 100644 (file)
index 0000000..a78791b
Binary files /dev/null and b/framework/Kolab_Filter/locale/de_DE/LC_MESSAGES/Kolab_Filter.mo differ
diff --git a/framework/Kolab_Filter/locale/fr_FR/LC_MESSAGES/Kolab_Filter.mo b/framework/Kolab_Filter/locale/fr_FR/LC_MESSAGES/Kolab_Filter.mo
new file mode 100644 (file)
index 0000000..4e99bed
Binary files /dev/null and b/framework/Kolab_Filter/locale/fr_FR/LC_MESSAGES/Kolab_Filter.mo differ
diff --git a/framework/Kolab_Filter/locale/nl_NL/LC_MESSAGES/Kolab_Filter.mo b/framework/Kolab_Filter/locale/nl_NL/LC_MESSAGES/Kolab_Filter.mo
new file mode 100644 (file)
index 0000000..5cbec8d
Binary files /dev/null and b/framework/Kolab_Filter/locale/nl_NL/LC_MESSAGES/Kolab_Filter.mo differ
diff --git a/framework/Kolab_Filter/package.xml b/framework/Kolab_Filter/package.xml
new file mode 100644 (file)
index 0000000..c614abf
--- /dev/null
@@ -0,0 +1,473 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Kolab_Filter</name>
+ <channel>pear.horde.org</channel>
+ <summary>Postfix filters for the Kolab server</summary>
+ <description>The filters provided by this package implement the Kolab
+ server resource management as well as some Kolab server sender
+ policies.
+ </description>
+ <lead>
+  <name>Gunnar Wrobel</name>
+  <user>wrobel</user>
+  <email>p@rdus.de</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-11-16</date>
+ <version>
+  <release>0.2.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+ <notes>
+  * Adapted the Kolab auth driver to the changes in Kolab_Server.
+  * Fixed handling of whole day invitations.
+  * Extended iTip reply delivery to support different transport mechanisms.
+  * Splitted Free/Busy functionality in a separate driver class.
+  * kolab/issue973 (Rewritten from shown inconveniently in kontact)
+  * kolab/issue3594 (Mail containing NUL byte not delivered, Kolab
+    Filter does not report lmtp error)
+  * kolab/issue3965 (Always accept: invitations get lost when Calendar
+    folder ist not writable for the calendar user)
+ </notes>
+ <contents>
+  <dir name="/">
+   <file name="COPYING" role="doc" />
+   <dir name="doc">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Filter">
+       <file name="kolabfilter.1" role="doc" />
+      </dir> <!-- /doc/Horde/Kolab/Filter -->
+     </dir> <!-- /doc/Horde/Kolab -->
+    </dir> <!-- /doc/Horde -->
+   </dir> <!-- /doc -->
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Test">
+       <file name="Filter.php" role="php" />
+      </dir> <!-- /lib/Horde/Kolab/Test -->
+      <file name="Resource.php" role="php" />
+      <dir name="Filter">
+       <file name="Content.php" role="php" />
+       <file name="Base.php" role="php" />
+       <file name="Incoming.php" role="php" />
+       <file name="Outlook.php" role="php" />
+       <file name="Response.php" role="php" />
+       <file name="Transport.php" role="php" />
+       <dir name="Transport">
+        <file name="drop.php" role="php" />
+        <file name="DovecotLDA.php" role="php" />
+        <file name="echo.php" role="php" />
+        <file name="lda.php" role="php" />
+        <file name="lmtp.php" role="php" />
+        <file name="LMTPTLS.php" role="php" />
+        <file name="smtp.php" role="php" />
+        <file name="stdout.php" role="php" />
+       </dir> <!-- /lib/Horde/Kolab/Filter/Transport -->
+      </dir> <!-- /lib/Horde/Kolab/Filter -->
+      <dir name="Resource">
+       <file name="Freebusy.php" role="php" />
+       <dir name="Freebusy">
+        <file name="Mock.php" role="php" />
+        <file name="Kolab.php" role="php" />
+       </dir> <!-- /lib/Horde/Kolab/Resource/Freebusy -->
+      </dir> <!-- /lib/Horde/Kolab/Resource -->
+     </dir> <!-- /lib/Horde/Kolab -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="locale" baseinstalldir="/">
+    <dir name="de_DE">
+     <dir name="LC_MESSAGES">
+      <file name="Kolab_Filter.mo" role="data" />
+     </dir> <!-- /locale/de_DE/LC_MESSAGES -->
+    </dir> <!-- /locale/de_DE -->
+    <dir name="fr_FR">
+     <dir name="LC_MESSAGES">
+      <file name="Kolab_Filter.mo" role="data" />
+     </dir> <!-- /locale/fr_FR/LC_MESSAGES -->
+    </dir> <!-- /locale/fr_FR -->
+    <dir name="nl_NL">
+     <dir name="LC_MESSAGES">
+      <file name="Kolab_Filter.mo" role="data" />
+     </dir> <!-- /locale/nl_NL/LC_MESSAGES -->
+    </dir> <!-- /locale/nl_NL -->
+   </dir> <!-- /locale -->
+   <dir name="po" baseinstalldir="/">
+    <file name="Kolab_Filter.pot" role="data" />
+    <file name="de_DE.po" role="data" />
+    <file name="fr_FR.po" role="data" />
+    <file name="nl_NL.po" role="data" />
+   </dir> <!-- /po -->
+   <dir name="script" baseinstalldir="/">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Filter">
+       <file name="kolabfilter.php" role="script">
+        <replace from="@php_bin@" to="php_bin" type="pear-config"/>
+       </file>
+       <file name="kolabmailboxfilter.php" role="script">
+        <replace from="@php_bin@" to="php_bin" type="pear-config"/>
+       </file>
+      </dir> <!-- /script/Horde/Kolab/Filter -->
+     </dir> <!-- /script/Horde/Kolab -->
+    </dir> <!-- /script/Horde -->
+   </dir> <!-- /scripts -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Filter">
+       <file name="AllTests.php" role="test" />
+       <file name="FilterTest.php" role="test" />
+       <file name="ContentTest.php" role="test" />
+       <file name="IncomingTest.php" role="test" />
+       <file name="LoadTest.php" role="test" />
+       <file name="ResourceTest.php" role="test" />
+       <dir name="fixtures">
+        <file name="attendee_status_invitation.eml" role="test" />
+        <file name="empty.eml" role="test" />
+        <file name="empty2.ret" role="test" />
+        <file name="forged.eml" role="test" />
+        <file name="forged.ret" role="test" />
+        <file name="forged_trans.ret" role="test" />
+        <file name="invitation_forward.eml" role="test" />
+        <file name="invitation_forward.ret" role="test" />
+        <file name="longstring_invitation.eml" role="test" />
+        <file name="null.ret" role="test" />
+        <file name="privileged.ret" role="test" />
+        <file name="recur_invitation.eml" role="test" />
+        <file name="recur_invitation2.eml" role="test" />
+        <file name="recur_invitation.ret" role="test" />
+        <file name="simple.eml" role="test" />
+        <file name="simple.ret" role="test" />
+        <file name="simple2.ret" role="test" />
+        <file name="simple_out.ret" role="test" />
+        <file name="test.eml" role="test" />
+        <file name="tiny.eml" role="test" />
+        <file name="tiny.ret" role="test" />
+        <file name="vacation.eml" role="test" />
+        <file name="vacation.ret" role="test" />
+        <file name="validation.eml" role="test" />
+        <file name="validation.ret" role="test" />
+       </dir> <!-- /test/Horde/Kolab/Filter/fixtures -->
+      </dir> <!-- /test/Horde/Kolab/Filter -->
+     </dir> <!-- /test/Horde/Kolab -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde</name>
+    <channel>pear.horde.org</channel>
+    <min>0.0.2</min>
+   </package>
+   <package>
+    <name>Horde_iCalendar</name>
+    <channel>pear.horde.org</channel>
+    <min>0.0.3</min>
+   </package>
+   <package>
+    <name>Argv</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_MIME</name>
+    <channel>pear.horde.org</channel>
+    <min>0.0.2</min>
+   </package>
+   <package>
+    <name>Horde_Util</name>
+    <channel>pear.horde.org</channel>
+    <min>0.0.2</min>
+   </package>
+   <package>
+    <name>Kolab_Server</name>
+    <channel>pear.horde.org</channel>
+    <min>0.2.0</min>
+   </package>
+  </required>
+  <optional>
+   <!-- Both are only required for testing -->
+   <package>
+    <name>Horde_Notification</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_Prefs</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="doc/Horde/Kolab/Filter/kolabfilter.1" as="man/man1/kolabfilter.1" />
+   <install name="lib/Horde/Kolab/Resource.php" as="Horde/Kolab/Resource.php" />
+   <install name="lib/Horde/Kolab/Resource/Freebusy.php" as="Horde/Kolab/Resource/Freebusy.php" />
+   <install name="lib/Horde/Kolab/Resource/Freebusy/Kolab.php" as="Horde/Kolab/Resource/Freebusy/Kolab.php" />
+   <install name="lib/Horde/Kolab/Resource/Freebusy/Mock.php" as="Horde/Kolab/Resource/Freebusy/Mock.php" />
+   <install name="lib/Horde/Kolab/Filter/Content.php" as="Horde/Kolab/Filter/Content.php" />
+   <install name="lib/Horde/Kolab/Test/Filter.php" as="Horde/Kolab/Test/Filter.php" />
+   <install name="lib/Horde/Kolab/Filter/Base.php" as="Horde/Kolab/Filter/Base.php" />
+   <install name="lib/Horde/Kolab/Filter/Incoming.php" as="Horde/Kolab/Filter/Incoming.php" />
+   <install name="lib/Horde/Kolab/Filter/Outlook.php" as="Horde/Kolab/Filter/Outlook.php" />
+   <install name="lib/Horde/Kolab/Filter/Response.php" as="Horde/Kolab/Filter/Response.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport.php" as="Horde/Kolab/Filter/Transport.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/DovecotLDA.php" as="Horde/Kolab/Filter/Transport/DovecotLDA.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/drop.php" as="Horde/Kolab/Filter/Transport/drop.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/echo.php" as="Horde/Kolab/Filter/Transport/echo.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/lda.php" as="Horde/Kolab/Filter/Transport/lda.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/lmtp.php" as="Horde/Kolab/Filter/Transport/lmtp.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/LMTPTLS.php" as="Horde/Kolab/Filter/Transport/LMTPTLS.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/smtp.php" as="Horde/Kolab/Filter/Transport/smtp.php" />
+   <install name="lib/Horde/Kolab/Filter/Transport/stdout.php" as="Horde/Kolab/Filter/Transport/stdout.php" />
+   <install name="script/Horde/Kolab/Filter/kolabmailboxfilter.php" as="kolabmailboxfilter" />
+   <install name="script/Horde/Kolab/Filter/kolabfilter.php" as="kolabfilter" />
+   <install name="test/Horde/Kolab/Filter/AllTests.php" as="Horde/Kolab/Filter/AllTests.php" />
+   <install name="test/Horde/Kolab/Filter/ContentTest.php" as="Horde/Kolab/Filter/ContentTest.php" />
+   <install name="test/Horde/Kolab/Filter/FilterTest.php" as="Horde/Kolab/Filter/FilterTest.php" />
+   <install name="test/Horde/Kolab/Filter/IncomingTest.php" as="Horde/Kolab/Filter/IncomingTest.php" />
+   <install name="test/Horde/Kolab/Filter/LoadTest.php" as="Horde/Kolab/Filter/LoadTest.php" />
+   <install name="test/Horde/Kolab/Filter/ResourceTest.php" as="Horde/Kolab/Filter/ResourceTest.php" />
+   <install name="test/Horde/Kolab/Filter/fixtures/attendee_status_invitation.eml" as="Horde/Kolab/Filter/fixtures/attendee_status_invitation.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/empty.eml" as="Horde/Kolab/Filter/fixtures/empty.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/empty2.ret" as="Horde/Kolab/Filter/fixtures/empty2.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/forged.eml" as="Horde/Kolab/Filter/fixtures/forged.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/forged.ret" as="Horde/Kolab/Filter/fixtures/forged.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/forged_trans.ret" as="Horde/Kolab/Filter/fixtures/forged_trans.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/invitation_forward.eml" as="Horde/Kolab/Filter/fixtures/invitation_forward.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/invitation_forward.ret" as="Horde/Kolab/Filter/fixtures/invitation_forward.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/longstring_invitation.eml" as="Horde/Kolab/Filter/fixtures/longstring_invitation.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/null.ret" as="Horde/Kolab/Filter/fixtures/null.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/privileged.ret" as="Horde/Kolab/Filter/fixtures/privileged.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/recur_invitation.eml" as="Horde/Kolab/Filter/fixtures/recur_invitation.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/recur_invitation2.eml" as="Horde/Kolab/Filter/fixtures/recur_invitation2.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/recur_invitation.ret" as="Horde/Kolab/Filter/fixtures/recur_invitation.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/simple.eml" as="Horde/Kolab/Filter/fixtures/simple.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/simple.ret" as="Horde/Kolab/Filter/fixtures/simple.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/simple2.ret" as="Horde/Kolab/Filter/fixtures/simple2.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/simple_out.ret" as="Horde/Kolab/Filter/fixtures/simple_out.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/test.eml" as="Horde/Kolab/Filter/fixtures/test.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/tiny.eml" as="Horde/Kolab/Filter/fixtures/tiny.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/tiny.ret" as="Horde/Kolab/Filter/fixtures/tiny.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/vacation.eml" as="Horde/Kolab/Filter/fixtures/vacation.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/vacation.ret" as="Horde/Kolab/Filter/fixtures/vacation.ret" />
+   <install name="test/Horde/Kolab/Filter/fixtures/validation.eml" as="Horde/Kolab/Filter/fixtures/validation.eml" />
+   <install name="test/Horde/Kolab/Filter/fixtures/validation.ret" as="Horde/Kolab/Filter/fixtures/validation.ret" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2009-11-16</date>
+   <version>
+    <release>0.1.7</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * LDAP errors should be marked as temporary failures.
+    * kolab/issue3464 (kolab-filter inserts double encoded utf-8 code
+      into From: header)
+    * kolab/issue3616 (automatic replies to invitations contain
+      127.0.0.1 in Message-ID)
+    * kolab/issue3364 (manpages for kolabfilter and kolabmailboxfilter)
+    * kolab/issue3768 (Kolab server 2.2.2 resmgr doesn't copy attendee
+      status)
+    * kolab/issue3767 (Kolab server 2.2.0 resmgr creates incorrect event
+      attendee status values)
+    * kolab/issue3610 (Mail to unknown@something.example.com gets
+      accepted and yields kolabmailboxfilter exit 38)
+    * kolab/issue2499 (Notification messages by the resource manager
+      should be localized)
+    * kolab/issue2544 (Encoding problem in cancelled appointments)
+    * Reverted the fix for kolab/issue2495 (The reply to an invitation
+      with umlauts of a group account doesn't display the umlauts.) It
+      should be covered by the fix for kolab/issue2499.
+    * Added dutch translation. Submitted by Richard Bos
+      (ml@radoeka.nl).
+   </notes>
+  </release>
+  <release>
+   <date>2009-11-07</date>
+   <version>
+    <release>0.1.6</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * Added french translation. Submitted by Mathieu Parent
+      (mathieuparent@users.sourceforge.net).
+    * kolab-filter inserts wrong/undecodeable utf-8 code into From: header
+      (kolab/issue3464, https://issues.kolab.org/issue3464)
+    * Notification messages by the resource manager should be localized
+      (kolab/issue2499, https://issues.kolab.org/issue2499)
+    * Whole-day events broken for resources (kolab/issue3558,
+      https://www.intevation.de/roundup/kolab/issue3558)
+    * Umlauts broken in automatically accepted events (kolab/issue3568,
+      https://www.intevation.de/roundup/kolab/issue3568)
+    * kolabmailboxfilter does not accept mail for
+      user+extension@example.com (kolab/issue3521,
+      https://www.intevation.de/roundup/kolab/issue3521)
+    * php error when inviting outside of free/busy interval
+      (kolab/issue3560, https://www.intevation.de/roundup/kolab/issue3560)
+    * Resmgr eats up all memory and dies on some recuring events
+      (https://issues.kolab.org/issue3868)
+   </notes>
+  </release>
+  <release>
+   <date>2009-03-06</date>
+   <version>
+    <release>0.1.5</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * Resources with policy "always accept" do not work if domain != kolabhost
+      (kolab/issue3441, https://www.intevation.de/roundup/kolab/issue3441)
+    * Resource booking fails if TRANSP:TRANSPARENT in the invitation
+      (kolab/issue1336, https://www.intevation.de/roundup/kolab/issue1336)
+    * Implemented simple resource locking (kolab/issue1382,
+      https://www.intevation.de/roundup/kolab/issue1382)
+    * Kolab_Filter dies when it should accept but has no Calender folder access
+      (kolab/issue3289, https://www.intevation.de/roundup/kolab/issue3289)
+    * Delivery of invitations fails with no kolabInvitationPolicy is in ldap,
+      (kolab/issue3435, https://www.intevation.de/roundup/kolab/issue3435)
+    * Ensure that the Outlook iCal handler adds the ORGANIZER of an
+      event and fix reinjection port for rewritten invitations
+      (kolab/issue3192, https://www.intevation.de/roundup/kolab/issue3192)
+   </notes>
+  </release>
+  <release>
+   <date>2009-02-24</date>
+   <version>
+    <release>0.1.4</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * Fixed log location for fatal errors (kolab/issue3426,
+      https://www.intevation.de/roundup/kolab/issue3426)
+    * Fixed invalid function call in Content.php (kolab/issue3426,
+      https://www.intevation.de/roundup/kolab/issue3426)
+    * Fixed unnecessary large test file (kolab/issue3299,
+      https://www.intevation.de/roundup/kolab/issue3299)
+    * Extended testing.
+    * Added man page (kolab/issue3364,
+      https://www.intevation.de/roundup/kolab/issue3364)
+    * Added string translations (kolab/issue2499,
+      https://www.intevation.de/roundup/kolab/issue2499)
+    * Fixed the Outlook iCal handler (kolab/issue3192,
+      https://www.intevation.de/roundup/kolab/issue3192)
+   </notes>
+  </release>
+  <release>
+   <date>2008-12-12</date>
+   <version>
+    <release>0.1.3</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * Fixed Dovecot delivery.
+    * Fixed copyright information.
+   </notes>
+  </release>
+  <release>
+   <date>2008-12-05</date>
+   <version>
+    <release>0.1.2</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+    * kolab/issue3260 (kolabfilter does not allow empty sender (and therefore
+      MAILER-DAEMON))
+    * kolab/issue3256 (resmgr responses should reflect server revision in PRODID)
+    * Partial fix for kolab/issue3289 (resmgr dies when it should accept, but has
+      not Calender folder access)
+   </notes>
+  </release>
+  <release>
+   <date>2008-11-25</date>
+   <version>
+    <release>0.1.1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Fixed retrival of the user data.
+     * Corrected some minor errors from restructuring the package.
+   </notes>
+  </release>
+  <release>
+   <date>2008-10-29</date>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Preparations for an initial release.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Kolab_Filter/po/Kolab_Filter.pot b/framework/Kolab_Filter/po/Kolab_Filter.pot
new file mode 100644 (file)
index 0000000..070ff2c
--- /dev/null
@@ -0,0 +1,109 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Horde Project
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: dev@lists.horde.org\n"
+"POT-Creation-Date: 2008-12-14 17:09+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:887
+#: lib/Horde/Kolab/Resource.php:887
+#, php-format
+msgid "%s has accepted the invitation to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:886
+#: lib/Horde/Kolab/Resource.php:886
+#, php-format
+msgid "%s has accepted the update to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:621
+#: lib/Horde/Kolab/Resource.php:621
+#, php-format
+msgid "%s has been cancelled"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:877
+#: lib/Horde/Kolab/Resource.php:877
+#, php-format
+msgid "%s has declined the invitation to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:876
+#: lib/Horde/Kolab/Resource.php:876
+#, php-format
+msgid "%s has declined the update to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:897
+#: lib/Horde/Kolab/Resource.php:897
+#, php-format
+msgid "%s has tentatively accepted the invitation to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:896
+#: lib/Horde/Kolab/Resource.php:896
+#, php-format
+msgid "%s has tentatively accepted the update to the following event:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:403
+#: lib/Horde/Kolab/Filter/Content.php:403
+#, php-format
+msgid "(UNTRUSTED, sender <%s> is not authenticated)"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:397
+#: lib/Horde/Kolab/Filter/Content.php:397
+#, php-format
+msgid "(UNTRUSTED, sender is <%s>)"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:888
+#: lib/Horde/Kolab/Resource.php:888
+msgid "Accepted: "
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:878
+#: lib/Horde/Kolab/Resource.php:878
+msgid "Declined: "
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:603
+#: lib/Horde/Kolab/Resource.php:603
+#, php-format
+msgid "Error processing \"%s\""
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:825
+#: lib/Horde/Kolab/Resource.php:825
+msgid "No summary available"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:898
+#: lib/Horde/Kolab/Resource.php:898
+msgid "Tentative: "
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:620
+#: lib/Horde/Kolab/Resource.php:620
+msgid "The following event has been successfully removed:"
+msgstr ""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:602
+#: lib/Horde/Kolab/Resource.php:602
+#, php-format
+msgid "The following event that was canceled is not present in %s's calendar:"
+msgstr ""
diff --git a/framework/Kolab_Filter/po/de_DE.po b/framework/Kolab_Filter/po/de_DE.po
new file mode 100644 (file)
index 0000000..e643372
--- /dev/null
@@ -0,0 +1,109 @@
+# German translations for Kolab_Filter package
+# German messages for Kolab_Filter.
+# Copyright (C) 2008 KDAB
+# This file is distributed under the same license as the Kolab_Filter package.
+# Gunnar Wrobel <p@rdus.de>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab_Filter\n"
+"Report-Msgid-Bugs-To: p@rdus.de\n"
+"POT-Creation-Date: 2008-12-14 17:09+0100\n"
+"PO-Revision-Date: 2008-12-14 17:58+0100\n"
+"Last-Translator:  <p@rdus.de>\n"
+"Language-Team: German\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:887
+#: lib/Horde/Kolab/Resource.php:887
+#, php-format
+msgid "%s has accepted the invitation to the following event:"
+msgstr "%s hat die Einladung für folgenden Termin akzeptiert:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:886
+#: lib/Horde/Kolab/Resource.php:886
+#, php-format
+msgid "%s has accepted the update to the following event:"
+msgstr "%s hat die Aktualisierung für folgenden Termin akzeptiert:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:621
+#: lib/Horde/Kolab/Resource.php:621
+#, php-format
+msgid "%s has been cancelled"
+msgstr "%s wurde storniert."
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:877
+#: lib/Horde/Kolab/Resource.php:877
+#, php-format
+msgid "%s has declined the invitation to the following event:"
+msgstr "%s hat die Einladung für folgenden Termin abgelehnt:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:876
+#: lib/Horde/Kolab/Resource.php:876
+#, php-format
+msgid "%s has declined the update to the following event:"
+msgstr "%s hat die Aktualisierung für folgenden Termin abgelehnt:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:897
+#: lib/Horde/Kolab/Resource.php:897
+#, php-format
+msgid "%s has tentatively accepted the invitation to the following event:"
+msgstr "%s hat die Einladung für folgenden Termin unter Vorbehalt akzeptiert:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:896
+#: lib/Horde/Kolab/Resource.php:896
+#, php-format
+msgid "%s has tentatively accepted the update to the following event:"
+msgstr "%s hat die Aktualisierung für folgenden Termin unter Vorbehalt akzeptiert:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:403
+#: lib/Horde/Kolab/Filter/Content.php:403
+#, php-format
+msgid "(UNTRUSTED, sender <%s> is not authenticated)"
+msgstr "(NICHT VERTRAUENSWÜRDIG, der Absender <%s> ist nicht authentifiziert)"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:397
+#: lib/Horde/Kolab/Filter/Content.php:397
+#, php-format
+msgid "(UNTRUSTED, sender is <%s>)"
+msgstr "(NICHT VERTRAUENSWÜRDIG, der Absender ist <%s>)"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:888
+#: lib/Horde/Kolab/Resource.php:888
+msgid "Accepted: "
+msgstr "Akzeptiert: "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:878
+#: lib/Horde/Kolab/Resource.php:878
+msgid "Declined: "
+msgstr "Abgelehnt: "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:603
+#: lib/Horde/Kolab/Resource.php:603
+#, php-format
+msgid "Error processing \"%s\""
+msgstr "Fehler beim Verarbeiten von \"%s\""
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:825
+#: lib/Horde/Kolab/Resource.php:825
+msgid "No summary available"
+msgstr "Keine Zusammenfassung verfügbar"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:898
+#: lib/Horde/Kolab/Resource.php:898
+msgid "Tentative: "
+msgstr "Unter Vorbehalt: "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:620
+#: lib/Horde/Kolab/Resource.php:620
+msgid "The following event has been successfully removed:"
+msgstr "Der folgende Termin wurde erfolgreich gelöscht:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:602
+#: lib/Horde/Kolab/Resource.php:602
+#, php-format
+msgid "The following event that was canceled is not present in %s's calendar:"
+msgstr "Es wurde versucht den folgenden Termin zu löschen, aber dieser ist im Kalender von %s nicht vorhanden:"
diff --git a/framework/Kolab_Filter/po/fr_FR.po b/framework/Kolab_Filter/po/fr_FR.po
new file mode 100644 (file)
index 0000000..e6c3d42
--- /dev/null
@@ -0,0 +1,61 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab_Filter\n"
+"Report-Msgid-Bugs-To: p@rdus.de\n"
+"POT-Creation-Date: 2008-12-14 17:09+0100\n"
+"PO-Revision-Date: 2009-03-17 11:44+0100\n"
+"Last-Translator: Mathieu Parent <math.parent@gmail.com>\n"
+"Language-Team: French\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=iso-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);"
+
+msgid "%s has accepted the invitation to the following event:"
+msgstr "%s a accepté l'invitation pour l'évenement suivant :"
+
+msgid "%s has accepted the update to the following event:"
+msgstr "%s a accepté la mise Ã  jour de l'évenement suivant :"
+
+msgid "%s has been cancelled"
+msgstr "%s a Ã©té annulé."
+
+msgid "%s has declined the invitation to the following event:"
+msgstr "%s a refusé l'invitation pour l'évenement suivant :"
+
+msgid "%s has declined the update to the following event:"
+msgstr "%s a refusé la mise Ã  jour de l'évenement suivant :"
+
+msgid "%s has tentatively accepted the invitation to the following event:"
+msgstr "%s a provisoirement accepté l'invitation pour l'évenement suivant :"
+
+msgid "%s has tentatively accepted the update to the following event:"
+msgstr "%s a provisoirement accepté la mise Ã  jour pour l'évenement suivant :"
+
+msgid "(UNTRUSTED, sender <%s> is not authenticated)"
+msgstr "(SUSPECT, l'expéditeur <%s> n'est pas authentifié)"
+
+msgid "(UNTRUSTED, sender is <%s>)"
+msgstr "(SUSPECT, l'expéditeur est <%s>)"
+
+msgid "Accepted: "
+msgstr "Accepté : "
+
+msgid "Declined: "
+msgstr "Refusé : "
+
+msgid "Error processing \"%s\""
+msgstr "Erreur lors du traitement de \"%s\""
+
+msgid "No summary available"
+msgstr "Résumé non disponible"
+
+msgid "Tentative: "
+msgstr "Tentative : "
+
+msgid "The following event has been successfully removed:"
+msgstr "L'évenement suivant a Ã©té enlevé avec succès :"
+
+msgid "The following event that was canceled is not present in %s's calendar:"
+msgstr ""
+"L'évenement suivant qui a Ã©té annulé n'est pas présent dans l'agenda de %s :"
diff --git a/framework/Kolab_Filter/po/nl_NL.po b/framework/Kolab_Filter/po/nl_NL.po
new file mode 100644 (file)
index 0000000..58e4e36
--- /dev/null
@@ -0,0 +1,109 @@
+# Dutch translations for Kolab_Filter package
+# Copyright (C) 2009 Richard Bos
+# This file is distributed under the same license as the Kolab_Filter package.
+# Richard Bos <ml@radoeka.nl>, 2009
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab_Filter\n"
+"Report-Msgid-Bugs-To: p@rdus.de\n"
+"POT-Creation-Date: 2008-12-14 17:09+0100\n"
+"PO-Revision-Date: 2009-03-17 11:44+0100\n"
+"Last-Translator: Richard Bos <ml@radoeka.nl>\n"
+"Language-Team: Dutch\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:887
+#: lib/Horde/Kolab/Resource.php:887
+#, php-format
+msgid "%s has accepted the invitation to the following event:"
+msgstr "%s heeft de uitnodiging voor de volgende gebeurtenis geaccepteerd:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:886
+#: lib/Horde/Kolab/Resource.php:886
+#, php-format
+msgid "%s has accepted the update to the following event:"
+msgstr "%s heeft de wijziging voor de volgende gebeurtenis geaccepteerd:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:621
+#: lib/Horde/Kolab/Resource.php:621
+#, php-format
+msgid "%s has been cancelled"
+msgstr "%s is afgelast"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:877
+#: lib/Horde/Kolab/Resource.php:877
+#, php-format
+msgid "%s has declined the invitation to the following event:"
+msgstr "%s heeft de uitnodiging voor de volgende gebeurtenis afgewezen:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:876
+#: lib/Horde/Kolab/Resource.php:876
+#, php-format
+msgid "%s has declined the update to the following event:"
+msgstr "%s heeft de wijziging voor de volgende gebeurtenis afgewezen:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:897
+#: lib/Horde/Kolab/Resource.php:897
+#, php-format
+msgid "%s has tentatively accepted the invitation to the following event:"
+msgstr "%s heeft de uitnodiging voor de volgende gebeurtenis voorlopig accepteerd:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:896
+#: lib/Horde/Kolab/Resource.php:896
+#, php-format
+msgid "%s has tentatively accepted the update to the following event:"
+msgstr "%s heeft de wijziging voor de volgende gebeurtenis voorlopig accepteerd:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:403
+#: lib/Horde/Kolab/Filter/Content.php:403
+#, php-format
+msgid "(UNTRUSTED, sender <%s> is not authenticated)"
+msgstr "(NIET VERTROUWD, afzender <%s> is niet geauthenticeerd)"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Filter/Content.php:397
+#: lib/Horde/Kolab/Filter/Content.php:397
+#, php-format
+msgid "(UNTRUSTED, sender is <%s>)"
+msgstr "(NIET VERTROUWD, afzender <%s>)"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:888
+#: lib/Horde/Kolab/Resource.php:888
+msgid "Accepted: "
+msgstr "Geaccepteerd: "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:878
+#: lib/Horde/Kolab/Resource.php:878
+msgid "Declined: "
+msgstr "Afgewezen: "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:603
+#: lib/Horde/Kolab/Resource.php:603
+#, php-format
+msgid "Error processing \"%s\""
+msgstr "Fout tijdens het bewerken "
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:825
+#: lib/Horde/Kolab/Resource.php:825
+msgid "No summary available"
+msgstr "Geen samenvatting beschikbaar"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:898
+#: lib/Horde/Kolab/Resource.php:898
+msgid "Tentative: "
+msgstr "Voorlopig"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:620
+#: lib/Horde/Kolab/Resource.php:620
+msgid "The following event has been successfully removed:"
+msgstr "De volgende gebeurtenis is succesvol verwijderd:"
+
+#: framework/Kolab_Filter/lib/Horde/Kolab/Resource.php:602
+#: lib/Horde/Kolab/Resource.php:602
+#, php-format
+msgid "The following event that was canceled is not present in %s's calendar:"
+msgstr "De volgende afgezegde gebeurtenis is niet beschikbaar in de agenda van %s: "
+
diff --git a/framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabfilter.php b/framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabfilter.php
new file mode 100644 (file)
index 0000000..1494e45
--- /dev/null
@@ -0,0 +1,30 @@
+#!@php_bin@
+<?php
+/**
+ *  A filter for outgoing mail on a Kolab Server. It rewrites headers
+ *  and handles Outlook issues.
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * $Horde: framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabfilter.php,v 1.3 2008/12/12 15:24:04 wrobel Exp $
+ *
+ * @package Kolab
+ */
+
+/** Kolab filter library */
+require_once 'Horde/Kolab/Filter/Content.php';
+require_once 'Horde/Kolab/Filter/Response.php';
+
+/* Parse the mail */
+$parser = &new Horde_Kolab_Filter_Content();
+$response = &new Horde_Kolab_Filter_Response();
+
+$result = $parser->parse();
+if (is_a($result, 'PEAR_Error')) {
+    $response->handle($result);
+}
+exit(0);
+?>
diff --git a/framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabmailboxfilter.php b/framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabmailboxfilter.php
new file mode 100644 (file)
index 0000000..695a224
--- /dev/null
@@ -0,0 +1,30 @@
+#!@php_bin@
+<?php
+/**
+ *  A filter for incoming mail on a Kolab Server. It checks the
+ *  messages for iCal data and handles automatic invitations.
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * $Horde: framework/Kolab_Filter/script/Horde/Kolab/Filter/kolabmailboxfilter.php,v 1.3 2008/12/12 15:24:04 wrobel Exp $
+ *
+ * @package Kolab
+ */
+
+/** Kolab filter library */
+require_once 'Horde/Kolab/Filter/Incoming.php';
+require_once 'Horde/Kolab/Filter/Response.php';
+
+/* Parse the mail */
+$parser = &new Horde_Kolab_Filter_Incoming();
+$response = &new Horde_Kolab_Filter_Response();
+
+$result = $parser->parse();
+if (is_a($result, 'PEAR_Error')) {
+    $response->handle($result);
+}
+exit(0);
+?>
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/AllTests.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/AllTests.php
new file mode 100644 (file)
index 0000000..dbeb39d
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * All tests for the Horde_Kolab_Filter:: package.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/AllTests.php,v 1.2 2009/01/06 17:49:21 jan Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ * Define the main method 
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Kolab_Filter_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * Combine the tests for this package.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/AllTests.php,v 1.2 2009/01/06 17:49:21 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Kolab_Filter');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Kolab_Filter_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Kolab_Filter_AllTests::main') {
+    Horde_Kolab_Filter_AllTests::main();
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/ContentTest.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/ContentTest.php
new file mode 100644 (file)
index 0000000..4e75ef1
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Test the content filter class within the Kolab filter implementation.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/ContentTest.php,v 1.9 2009/03/20 23:41:40 wrobel Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'Horde/Kolab/Test/Filter.php';
+
+require_once 'Horde.php';
+require_once 'Horde/Kolab/Filter/Content.php';
+
+/**
+ * Test the content filter.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/ContentTest.php,v 1.9 2009/03/20 23:41:40 wrobel Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_ContentTest extends Horde_Kolab_Test_Filter
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        $result = $this->prepareBasicSetup();
+
+        $this->server  = &$result['server'];
+        $this->storage = &$result['storage'];
+        $this->auth    = &$result['auth'];
+
+        global $conf;
+
+        $conf['kolab']['imap']['server'] = 'localhost';
+        $conf['kolab']['imap']['port']   = 0;
+        $conf['kolab']['imap']['allow_special_users'] = true;
+        $conf['kolab']['filter']['reject_forged_from_header'] = false;
+        $conf['kolab']['filter']['email_domain'] = 'example.org';
+        $conf['kolab']['filter']['privileged_networks'] = '127.0.0.1,192.168.0.0/16';
+        $conf['kolab']['filter']['verify_from_header'] = true;
+        $conf['kolab']['filter']['calendar_id'] = 'calendar';
+        $conf['kolab']['filter']['calendar_pass'] = 'calendar';
+        $conf['kolab']['filter']['lmtp_host'] = 'imap.example.org';
+        $conf['kolab']['filter']['simple_locks'] = true;
+        $conf['kolab']['filter']['simple_locks_timeout'] = 3;
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getNewFolder();
+        $folder->setName('Kalender');
+        $result = $folder->save(array('type' => 'event',
+                                      'default' => true));
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test sending messages through the content filter.
+     *
+     * @dataProvider addressCombinations
+     */
+    public function testContentHandler($infile, $outfile, $user, $client, $from,
+                                       $to, $host, $params = array())
+    {
+        $this->sendFixture($infile, $outfile, $user, $client, $from, $to,
+                           $host, $params);
+    }
+
+    /**
+     * Provides various test situations for the Kolab content filter.
+     */
+    public function addressCombinations()
+    {
+        return array(
+            /**
+             * Test a simple message
+             */
+            array(dirname(__FILE__) . '/fixtures/vacation.eml',
+                  dirname(__FILE__) . '/fixtures/vacation.ret',
+                  '', '', 'me@example.org', 'you@example.net', 'example.org',
+                  array('unmodified_content' => true)),
+            /**
+             * Test a simple message
+             */
+            array(dirname(__FILE__) . '/fixtures/tiny.eml',
+                  dirname(__FILE__) . '/fixtures/tiny.ret',
+                  '', '', 'me@example.org', 'you@example.org', 'example.org',
+                  array('unmodified_content' => true)),
+            /**
+             * Test a simple message
+             */
+            array(dirname(__FILE__) . '/fixtures/simple.eml',
+                  dirname(__FILE__) . '/fixtures/simple_out.ret',
+                  '', '', 'me@example.org', 'you@example.org', 'example.org',
+                  array('unmodified_content' => true)),
+            /**
+             * Test sending from a remote server without authenticating. This
+             * will be considered forging the sender.
+             */
+            array(dirname(__FILE__) . '/fixtures/forged.eml',
+                  dirname(__FILE__) . '/fixtures/forged.ret',
+                  '', '10.0.0.1', 'me@example.org', 'you@example.org', 'example.org',
+                  array('unmodified_content' => true)),
+            /**
+             * Test sending from a remote server without authenticating but
+             * within the priviledged network. This will not be considered
+             * forging the sender.
+             */
+            array(dirname(__FILE__) . '/fixtures/forged.eml',
+                  dirname(__FILE__) . '/fixtures/privileged.ret',
+                  '', '192.168.178.1', 'me@example.org', 'you@example.org', 'example.org',
+                  array('unmodified_content' => true)),
+            /**
+             * Test authenticated sending of a message from a remote client.
+             */
+            array(dirname(__FILE__) . '/fixtures/validation.eml',
+                  dirname(__FILE__) . '/fixtures/validation.ret',
+                  'me@example.org', 'remote.example.org', 'me@example.org', 'you@example.org', 'example.org'),
+            /**
+             * Test authenticated sending of a message from a remote client
+             * using an alias.
+             */
+            array(dirname(__FILE__) . '/fixtures/validation.eml',
+                  dirname(__FILE__) . '/fixtures/validation.ret',
+                  'me@example.org', 'remote.example.org', 'me.me@example.org', 'you@example.org', 'example.org'),
+            /**
+             * Test authenticated sending of a message from a remote client
+             * using an alias with capitals (MEME@example.org).
+             */
+            array(dirname(__FILE__) . '/fixtures/validation.eml',
+                  dirname(__FILE__) . '/fixtures/validation.ret',
+                  'me@example.org', 'remote.example.org', 'meme@example.org', 'you@example.org', 'example.org'),
+            /**
+             * Test authenticated sending of a message from a remote client
+             * as delegate
+             */
+            array(dirname(__FILE__) . '/fixtures/validation.eml',
+                  dirname(__FILE__) . '/fixtures/validation.ret',
+                  'me@example.org', 'remote.example.org', 'else@example.org', 'you@example.org', 'example.org'),
+            /**
+             * Test authenticated sending of a message from a remote client
+             * with an address that is not allowed.
+             */
+            array(dirname(__FILE__) . '/fixtures/validation.eml',
+                  dirname(__FILE__) . '/fixtures/validation.ret',
+                  'me@example.org', 'remote.example.org', 'else3@example.org', 'you@example.org', 'example.org',
+                  array('error' =>'Invalid From: header. else3@example.org looks like a forged sender')),
+            /**
+             * Test forwarding an invitation
+             */
+            array(dirname(__FILE__) . '/fixtures/invitation_forward.eml',
+                  dirname(__FILE__) . '/fixtures/invitation_forward.ret',
+                  'me@example.org', '10.0.2.1', 'me@example.org', 'you@example.org', 'example.org'),
+        );
+    }
+
+    /**
+     * Test rejecting a forged from header.
+     */
+    public function testRejectingForgedFromHeader()
+    {
+        global $conf;
+
+        $conf['kolab']['filter']['reject_forged_from_header'] = true;
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/forged.eml',
+                           dirname(__FILE__) . '/fixtures/forged.ret',
+                           '', '10.0.0.1', 'me@example.org', 'you@example.org', 'example.org',
+                           array('error' =>'Invalid From: header. me@example.org looks like a forged sender',
+                                 'unmodified_content' => true));
+    }
+
+    /**
+     * Test translated forged from headers.
+     */
+    public function testTranslatedForgedFromHeader()
+    {
+        global $conf;
+
+        $conf['kolab']['filter']['locale_path'] = dirname(__FILE__) . '/../../../../locale';
+        $conf['kolab']['filter']['locale'] = 'de_DE';
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/forged.eml',
+                           dirname(__FILE__) . '/fixtures/forged_trans.ret',
+                           '', '10.0.0.1', 'me@example.org', 'you@example.org', 'example.org',
+                           array('unmodified_content' => true));
+    }
+
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/FilterTest.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/FilterTest.php
new file mode 100644 (file)
index 0000000..111cda5
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Test the base filter class within the Kolab filter implementation.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/FilterTest.php,v 1.7 2009/02/24 11:48:41 wrobel Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+require_once 'PHPUnit/Extensions/OutputTestCase.php';
+
+require_once 'Horde.php';
+require_once 'Horde/Kolab/Filter/Incoming.php';
+
+/**
+ * Test the filter class.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/FilterTest.php,v 1.7 2009/02/24 11:48:41 wrobel Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_FilterTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        $GLOBALS['conf']['log']['enabled']          = false;
+
+        $_SERVER['SERVER_NAME'] = 'localhost';
+        $_SERVER['REMOTE_ADDR'] = 'ADDR';
+        $_SERVER['REMOTE_HOST'] = 'HOST';
+    }
+
+
+    /**
+     * Test incorrect usage of the Filter
+     */
+    public function testIncorrectUsage()
+    {
+        $_SERVER['argv'] = array($_SERVER['argv'][0]);
+        $parser   = &new Horde_Kolab_Filter_Incoming();
+        $inh = fopen(dirname(__FILE__) . '/fixtures/tiny.eml', 'r');
+        $result = $parser->parse($inh, 'echo');
+
+        $this->assertTrue(is_a($result, 'PEAR_Error'));
+
+        $this->assertContains('Please provide one or more recipients.', $result->getMessage());
+    }
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/IncomingTest.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/IncomingTest.php
new file mode 100644 (file)
index 0000000..d41cbed
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Test the incoming filter class within the Kolab filter implementation.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/IncomingTest.php,v 1.8 2009/03/20 23:41:40 wrobel Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/Filter.php';
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Extensions/OutputTestCase.php';
+
+require_once 'Horde.php';
+require_once 'Horde/Kolab/Filter/Incoming.php';
+
+/**
+ * Test the incoming filter.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/IncomingTest.php,v 1.8 2009/03/20 23:41:40 wrobel Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_IncomingTest extends Horde_Kolab_Test_Filter
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        global $conf;
+
+        $conf = array();
+
+        $test = new Horde_Kolab_Test_Filter();
+        $test->prepareBasicSetup();
+
+        $conf['log']['enabled']          = false;
+
+        $conf['kolab']['filter']['debug'] = true;
+
+        $conf['kolab']['imap']['server'] = 'localhost';
+        $conf['kolab']['imap']['port']   = 0;
+
+        $_SERVER['SERVER_NAME'] = 'localhost';
+    }
+
+
+    /**
+     * Test receiving the simple.eml message.
+     */
+    public function testSimpleIn()
+    {
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/simple.eml',
+                           dirname(__FILE__) . '/fixtures/simple2.ret',
+                           '', '', 'wrobel@example.org', 'me@example.org',
+                           'home.example.org', $params);
+    }
+
+    /**
+     * Test handling the line end with incoming messages.
+     */
+    public function testIncomingLineEnd()
+    {
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/empty.eml',
+                           dirname(__FILE__) . '/fixtures/empty2.ret',
+                           '', '127.0.0.1', 'wrobel@example.org', 'me@example.org',
+                           'home.example.org', $params);
+    }
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/LoadTest.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/LoadTest.php
new file mode 100644 (file)
index 0000000..2253eed
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Test the incoming filter class for its load behaviour..
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/LoadTest.php,v 1.8 2009/04/09 20:16:25 wrobel Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
+
+require_once 'Horde.php';
+require_once 'Horde/Kolab/Filter/Incoming.php';
+
+/**
+ * Test the incoming filter load.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/LoadTest.php,v 1.8 2009/04/09 20:16:25 wrobel Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_LoadTest extends PHPUnit_Extensions_PerformanceTestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        global $conf;
+
+        $conf = array();
+        $conf['log']['enabled']          = false;
+
+        $conf['kolab']['filter']['debug'] = true;
+
+        $conf['kolab']['server'] = array(
+            'driver' => 'test',
+            'params' => array(
+                'data' => array(
+                    'cn=me' => array(
+                        'dn' => 'cn=me',
+                        'data' => array(
+                            'objectClass' => array('kolabInetOrgPerson'),
+                            'mail' => array('me@example.com'),
+                            'kolabImapHost' => array('localhost'),
+                            'uid' => array('me'),
+                        )
+                    ),
+                    'cn=you' => array(
+                        'dn' => 'cn=you',
+                        'data' => array(
+                            'objectClass' => array('kolabInetOrgPerson'),
+                            'mail' => array('you@example.com'),
+                            'kolabImapHost' => array('localhost'),
+                            'uid' => array('you'),
+                        )
+                    ),
+                ),
+            )
+        );
+        $conf['kolab']['imap']['server'] = 'localhost';
+        $conf['kolab']['imap']['port']   = 0;
+        $conf['kolab']['imap']['allow_special_users'] = true;
+
+        $_SERVER['SERVER_NAME'] = 'localhost';
+   }
+
+
+    /**
+     * Test the time the script takes in handling some messages.
+     */
+    public function testLoad()
+    {
+        $this->setMaxRunningTime(3);
+
+        $tmpdir = Horde::getTempDir();
+        $tmpfile = @tempnam($tmpdir, 'BIG.eml.');
+        $tmpfh = @fopen($tmpfile, "w");
+        $head = file_get_contents(dirname(__FILE__) . '/fixtures/tiny.eml');
+        $body = '';
+        for ($i = 0; $i < 50000;$i++) {
+            $body .= md5(microtime());
+            if (($i % 2) == 0) {
+                $body .= "\n";
+            }
+        }
+        @fwrite($tmpfh, $head);
+        @fwrite($tmpfh, $body);
+        @fclose($tmpfh);
+
+        $_SERVER['argv'] = array($_SERVER['argv'][0], '--sender=me@example.com', '--recipient=you@example.com', '--user=', '--host=example.com');
+
+        for ($i = 0; $i < 10; $i++) {
+
+            $parser = &new Horde_Kolab_Filter_Incoming();
+            $inh = fopen(dirname(__FILE__) . '/fixtures/tiny.eml', 'r');
+            $parser->parse($inh, 'drop');
+
+            $parser = &new Horde_Kolab_Filter_Incoming();
+            $inh = fopen(dirname(__FILE__) . '/fixtures/simple.eml', 'r');
+            $parser->parse($inh, 'drop');
+
+            $parser = &new Horde_Kolab_Filter_Incoming();
+            $inh = fopen($tmpfile, 'r');
+            $parser->parse($inh, 'drop');
+
+        }
+    }
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/ResourceTest.php b/framework/Kolab_Filter/test/Horde/Kolab/Filter/ResourceTest.php
new file mode 100644 (file)
index 0000000..0849ecb
--- /dev/null
@@ -0,0 +1,336 @@
+<?php
+/**
+ * Test resource handling within the Kolab filter implementation.
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/ResourceTest.php,v 1.13 2009-11-16 19:01:33 wrobel Exp $
+ *
+ * @package Horde_Kolab_Filter
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/Filter.php';
+
+require_once 'Horde.php';
+require_once 'Horde/Kolab/Resource.php';
+require_once 'Horde/Kolab/Filter/Incoming.php';
+require_once 'Horde/iCalendar.php';
+require_once 'Horde/iCalendar/vfreebusy.php';
+
+/**
+ * Test resource handling
+ *
+ * $Horde: framework/Kolab_Filter/test/Horde/Kolab/Filter/ResourceTest.php,v 1.13 2009-11-16 19:01:33 wrobel Exp $
+ *
+ * Copyright 2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Horde_Kolab_Filter
+ */
+class Horde_Kolab_Filter_ResourceTest extends Horde_Kolab_Test_Filter
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        $result = $this->prepareBasicSetup();
+
+        $this->server  = &$result['server'];
+        $this->storage = &$result['storage'];
+        $this->auth    = &$result['auth'];
+
+        global $conf;
+
+        $conf['kolab']['imap']['server'] = 'localhost';
+        $conf['kolab']['imap']['port']   = 0;
+        $conf['kolab']['imap']['allow_special_users'] = true;
+        $conf['kolab']['filter']['reject_forged_from_header'] = false;
+        $conf['kolab']['filter']['email_domain'] = 'example.org';
+        $conf['kolab']['filter']['privileged_networks'] = '127.0.0.1,192.168.0.0/16';
+        $conf['kolab']['filter']['verify_from_header'] = true;
+        $conf['kolab']['filter']['calendar_id'] = 'calendar';
+        $conf['kolab']['filter']['calendar_pass'] = 'calendar';
+        $conf['kolab']['filter']['lmtp_host'] = 'imap.example.org';
+        $conf['kolab']['filter']['simple_locks'] = true;
+        $conf['kolab']['filter']['simple_locks_timeout'] = 3;
+
+        $conf['kolab']['filter']['itipreply']['driver'] = 'echo';
+        $conf['kolab']['filter']['itipreply']['params']['host'] = 'localhsot';
+        $conf['kolab']['filter']['itipreply']['params']['port'] = 25;
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getNewFolder();
+        $folder->setName('Kalender');
+        $result = $folder->save(array('type' => 'event',
+                                      'default' => true));
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test retrieval of the resource information
+     */
+    public function testGetResourceData()
+    {
+        $r = &new Kolab_Resource();
+        $d = $r->_getResourceData('test@example.org', 'wrobel@example.org');
+        $this->assertNoError($d);
+        $this->assertEquals('wrobel@example.org', $d['id']);
+        $this->assertEquals('home.example.org', $d['homeserver']);
+        $this->assertEquals('ACT_REJECT_IF_CONFLICTS', $d['action']);
+        $this->assertEquals('cn=Gunnar Wrobel', $d['cn']);
+    }
+
+    /**
+     * Test manual actions
+     */
+    public function testManual()
+    {
+        $r = &new Kolab_Resource();
+        $this->assertTrue($r->handleMessage('otherhost', 'test@example.org', 'wrobel@example.org', null));
+        $r = &new Kolab_Resource();
+        $this->assertTrue($r->handleMessage('localhost', 'test@example.org', 'wrobel@example.org', null));
+    }
+
+
+    /**
+     * Test invitation.
+     */
+    public function testRecurrenceInvitation()
+    {
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20080926T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20081126T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/recur_invitation.eml',
+                           dirname(__FILE__) . '/fixtures/recur_invitation.ret2',
+                           '', '', 'test@example.org', 'wrobel@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $this->assertEquals(1222419600, $events[0]['start-date']);
+
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test an that contains a long string.
+     */
+    public function testLongStringInvitation()
+    {
+        require_once 'Horde/iCalendar/vfreebusy.php';
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20080926T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20081126T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/longstring_invitation.eml',
+                           dirname(__FILE__) . '/fixtures/longstring_invitation.ret',
+                           '', '', 'test@example.org', 'wrobel@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $summaries = array();
+        foreach ($events as $event) {
+            $summaries[] = $event['summary'];
+        }
+        $this->assertContains('invitationtest2', $summaries);
+
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test an invitation that books a whole day.
+     */
+    public function testWholeDayInvitation()
+    {
+        require_once 'Horde/iCalendar/vfreebusy.php';
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20090401T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20090601T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/invitation_whole_day.eml',
+                           dirname(__FILE__) . '/fixtures/invitation_whole_day.ret',
+                           '', '', 'test@example.org', 'wrobel@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $summaries = array();
+        foreach ($events as $event) {
+            $summaries[] = $event['summary'];
+        }
+        $this->assertContains('issue3558', $summaries);
+
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test an invitation with plus addressing.
+     */
+    public function testInvitationWithPlusAddressing()
+    {
+        require_once 'Horde/iCalendar/vfreebusy.php';
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20090401T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20090601T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/invitation_plus_addressing.eml',
+                           dirname(__FILE__) . '/fixtures/invitation_plus_addressing.ret',
+                           '', '', 'test@example.org', 'wrobel+laptop@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $summaries = array();
+        foreach ($events as $event) {
+            $summaries[] = $event['summary'];
+        }
+        $this->assertContains('issue3521', $summaries);
+
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test invitation when no default has been given.
+     */
+    public function testRecurrenceNodefault()
+    {
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20080926T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20081126T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/recur_invitation.eml',
+                           dirname(__FILE__) . '/fixtures/recur_invitation.ret',
+                           '', '', 'wrobel@example.org', 'else@example.org', 
+                           'home.example.org', $params);
+    }
+
+    /**
+     * Test an issue with recurring invitations.
+     *
+     * https://issues.kolab.org/issue3868
+     */
+    public function testIssue3868()
+    {
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20090901T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20091101T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/recur_invitation2.eml',
+                           dirname(__FILE__) . '/fixtures/null.ret',
+                           '', '', 'test@example.org', 'wrobel@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $this->assertEquals(1251950400, $events[0]['start-date']);
+
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+    /**
+     * Test that the attendee status gets transferred.
+     */
+    public function testAttendeeStatusInvitation()
+    {
+        require_once 'Horde/iCalendar/vfreebusy.php';
+        $GLOBALS['KOLAB_FILTER_TESTING'] = &new Horde_iCalendar_vfreebusy();
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTSTART', Horde_iCalendar::_parseDateTime('20080926T000000Z'));
+        $GLOBALS['KOLAB_FILTER_TESTING']->setAttribute('DTEND', Horde_iCalendar::_parseDateTime('20081126T000000Z'));
+
+        $params = array('unmodified_content' => true,
+                        'incoming' => true);
+
+        $this->sendFixture(dirname(__FILE__) . '/fixtures/attendee_status_invitation.eml',
+                           dirname(__FILE__) . '/fixtures/null.ret',
+                           '', '', 'test@example.org', 'wrobel@example.org',
+                           'home.example.org', $params);
+
+        $result = $this->auth->authenticate('wrobel', array('password' => 'none'));
+        $this->assertNoError($result);
+
+        $folder = $this->storage->getFolder('INBOX/Kalender');
+        $data = $folder->getData();
+        $events = $data->getObjects();
+        $summaries = array();
+        foreach ($events as $event) {
+            foreach ($event['attendee'] as $attendee) {
+                switch ($attendee['smtp-address']) {
+                case 'needs@example.org':
+                    $this->assertEquals('none', $attendee['status']);
+                    break;
+                case 'accepted@example.org':
+                    $this->assertEquals('accepted', $attendee['status']);
+                    break;
+                case 'declined@example.org':
+                    $this->assertEquals('declined', $attendee['status']);
+                    break;
+                case 'tentative@example.org':
+                    $this->assertEquals('tentative', $attendee['status']);
+                    break;
+                case 'delegated@example.org':
+                    $this->assertEquals('none', $attendee['status']);
+                    break;
+                default:
+                    $this->fail('Unexpected attendee!');
+                    break;
+                }
+            }
+        }
+        $result = $data->deleteAll();
+        $this->assertNoError($result);
+    }
+
+}
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/attendee_status_invitation.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/attendee_status_invitation.eml
new file mode 100644 (file)
index 0000000..b85d166
--- /dev/null
@@ -0,0 +1,33 @@
+To: wrobel@example.org
+From: test@example.org
+Subject: Invitation kolab/issue3236
+Date: Fri, 21 Nov 2008 19:25:35 +0100
+MIME-Version: 1.0
+Content-Type: text/calendar;
+  method=request;
+  charset="utf-8"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:MAILTO:needs@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:MAILTO:accepted@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=DECLINED;RSVP=TRUE:MAILTO:declined@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;RSVP=TRUE:MAILTO:tentative@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=DELEGATED;RSVP=TRUE:MAILTO:delegated@example.org
+ORGANIZER:MAILTO:test@example.org
+DTSTART:20081122T190000Z
+DTEND:20081122T193000Z
+SEQUENCE:0
+UID:040000008200E00074C5B7101A82E0080000000020D533ED0E4CC9010000000000000000100
+ 00000094C5C0A65E8CC4DB1AEC47FD1255FCD
+DTSTAMP:20081121T182534Z
+DESCRIPTION:Zeit: Samstag\, 22. November 2008 20:00-20:30 (GMT+01:00)
+  Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\,
+  Wien.\\n\\n*~*~*~*~*~*~*~*~*~*\\n\\n\\n
+SUMMARY:invitationtest2
+END:VEVENT
+END:VCALENDAR
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.eml
new file mode 100644 (file)
index 0000000..7b36d3e
--- /dev/null
@@ -0,0 +1,5 @@
+From: me@example.com
+To:me@example.com
+Subject: xx
+
+xx
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty.ret
new file mode 100644 (file)
index 0000000..fd2471b
--- /dev/null
@@ -0,0 +1,9 @@
+Mail from sender: me@example.com
+Mail to recipient: me@example.com
+From: me@example.com
+To:me@example.com
+Subject: xx
+X-Kolab-Scheduling-Message: FALSE
+
+xx
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty2.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/empty2.ret
new file mode 100644 (file)
index 0000000..a4e5556
--- /dev/null
@@ -0,0 +1,9 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: me@example.org
+From: me@example.com
+To:me@example.com
+Subject: xx
+X-Kolab-Scheduling-Message: FALSE
+
+xx
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.eml
new file mode 100644 (file)
index 0000000..93f69a7
--- /dev/null
@@ -0,0 +1,14 @@
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: me@example.org
+To: "You" <you@example.org>
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged.ret
new file mode 100644 (file)
index 0000000..c1fd97b
--- /dev/null
@@ -0,0 +1,17 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: =?utf-8?B?IiAoVU5UUlVTVEVELCBzZW5kZXIgPG1lQGV4YW1wbGUub3JnPiBpcyBub3QgYXV0aGVudGljYXRlZCki?=<me@example.org>
+To: "You" <you@example.org>
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged_trans.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/forged_trans.ret
new file mode 100644 (file)
index 0000000..c1fd97b
--- /dev/null
@@ -0,0 +1,17 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: =?utf-8?B?IiAoVU5UUlVTVEVELCBzZW5kZXIgPG1lQGV4YW1wbGUub3JnPiBpcyBub3QgYXV0aGVudGljYXRlZCki?=<me@example.org>
+To: "You" <you@example.org>
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.eml
new file mode 100644 (file)
index 0000000..2d27198
--- /dev/null
@@ -0,0 +1,39 @@
+From: User1 <somebody@example.org>
+Sender: User2 <%1$s>
+To: User3 <%2$s>
+Subject: kolab/issue3192
+Date: Mon, 23 Feb 2009 15:16:14 +0100
+Message-ID: <000001c995c1$49c4a2a0$140ba8c0@invalid>
+MIME-Version: 1.0
+Content-Type: text/calendar; method=REQUEST;
+       charset="utf-8"
+Content-Transfer-Encoding: 7bit
+X-Mailer: Microsoft Office Outlook 11
+Thread-Index: AcmVuGgcYR9Pp2HUSHalFjSgcQETAwAAAq8gAAIyVwA=
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2670
+
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:%(user3)s
+DTSTART:20090223T140000Z
+DTEND:20090223T150000Z
+TRANSP:OPAQUE
+SEQUENCE:0
+UID:libkcal-910600374.601
+DTSTAMP:20090223T131221Z
+DESCRIPTION:Zeit: Montag\, 23. Februar 2009 15:00-16:00 (GMT+01:00)
+  Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\,
+  Wien.\n\n*~*~*~*~*~*~*~*~*~*\n\n\n\n>_____________________________________
+ ________\n>Von:       User1  \n>Gesendet:     Montag\, 23. Februar 2009
+  14:12\n>An:  User1\; %(user2)s\n>Betreff:    kolab/issue3192\n>Zeit:
+       Montag\, 23. Februar 2009 15:00-16:00 (GMT+01:00) Amsterdam\, Berlin\,
+  Bern\, Rom\, Stockholm\, Wien.\n>Ort:        \n>\n>\n
+SUMMARY:WG: kolab/issue3192
+PRIORITY:5
+X-MICROSOFT-CDO-IMPORTANCE:1
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_forward.ret
new file mode 100644 (file)
index 0000000..aa73dc7
--- /dev/null
@@ -0,0 +1,70 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+Message-ID: <20090305182508.176740qk2m2y26uc@localhost>
+Date: Mon, 23 Feb 2009 15:16:14 +0100
+Subject: kolab/issue3192
+From: me@example.org
+To: you@example.org
+X-Kolab-Forwarded: TRUE
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_4onv0mx6b56o"
+Content-Transfer-Encoding: 7bit
+
+This message is in MIME format.
+
+--=_5mtbys0ziy04
+Content-Type: text/plain;
+ charset=UTF-8
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+This is an invitation forwarded by outlook and
+was rectified by the Kolab server.
+The invitation was originally sent by
+User1 <somebody@example.org>.
+
+Diese Einladung wurde von Outlook weitergeleitet
+und vom Kolab-Server in gute Form gebracht.
+Die Einladung wurde urspr=C3=BCnglich von
+User1 <somebody@example.org> geschickt.
+--=_5mtbys0ziy04
+Content-Type: text/calendar;
+ charset=utf-8
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:user3)s
+DTSTART:20090223T140000Z
+DTEND:20090223T150000Z
+TRANSP:OPAQUE
+SEQUENCE:0
+UID:libkcal-910600374.601
+DTSTAMP:20090223T131221Z
+DESCRIPTION:Zeit:
+  Montag\, 23. Februar 2009 15:00-16:00 (GMT+01:00) Amsterdam\, Berlin\,
+  Bern\, Rom\, Stockholm\,
+  Wien.\n\n*~*~*~*~*~*~*~*~*~*\n\n\n\n>_____________________________________
+ ________\n>Von:
+       User1  \n>Gesendet:
+       Montag\, 23. Februar 2009 14:12\n>An:
+       User1\;
+  user2)s\n>Betreff:
+       kolab/issue3192\n>Zeit:
+       Montag\, 23. Februar 2009 15:00-16:00 (GMT+01:00) Amsterdam\, Berlin\,
+  Bern\, Rom\, Stockholm\, Wien.\n>Ort:        \n>\n>\n
+SUMMARY:WG: kolab/issue3192
+PRIORITY:5
+X-MICROSOFT-CDO-IMPORTANCE:1
+CLASS:PUBLIC
+ORGANIZER;CN=User1:mailto:somebody@example.org
+END:VEVENT
+END:VCALENDAR
+
+--=_5mtbys0ziy04--
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.eml
new file mode 100644 (file)
index 0000000..0046684
--- /dev/null
@@ -0,0 +1,31 @@
+To: wrobel+laptop@example.org
+From: test@example.org
+Subject: Invitation kolab/issue3558
+Date: Fri, 21 Nov 2008 19:25:35 +0100
+MIME-Version: 1.0
+Content-Type: text/calendar;
+  method=request;
+  charset="utf-8"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN
+VERSION:2.0 
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTAMP:20090416T100820Z
+ORGANIZER;CN="Test":MAILTO:test@example.com
+ATTENDEE;CN="Test";RSVP=FALSE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:
+ mailto:test@example.com 
+ATTENDEE;RSVP=TRUE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:
+ wrobel+laptop@example.com
+CREATED:20090416T100818Z
+UID:libkcal-865928855.207
+LAST-MODIFIED:20090416T100818Z
+SUMMARY:issue3521
+DTSTART;VALUE=DATE:20090417
+DTEND;VALUE=DATE:20090418
+TRANSP:OPAQUE
+END:VEVENT 
+
+END:VCALENDAR
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_plus_addressing.ret
new file mode 100644 (file)
index 0000000..8f1da42
--- /dev/null
@@ -0,0 +1,32 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: test@example.com
+Received: from localhost (localhost [127.0.0.1]) by localhost (Horde MIME library) with HTTP; Sat, 18 Apr 2009 21:46:38 +0200
+Message-ID: <20090418214638.lji09a8nk8ggg0os@localhost>
+Date: Sat, 18 Apr 2009 21:46:38 +0200
+From: cn=Gunnar Wrobel <wrobel@example.org>
+To: test@example.com
+Subject: Accepted: issue3521
+MIME-Version: 1.0
+Content-Type: text/calendar;
+       charset=UTF-8;
+       method="REPLY"
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+\r\n\r\nBEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//kolab.org//NONSGML Kolab Server 2//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:libkcal-865928855.207
+SUMMARY:issue3521
+DTSTART;VALUE=3DDATE:20090417
+DTEND;VALUE=3DDATE:20090418
+SEQUENCE:0
+ORGANIZER;CN=3DTest:MAILTO:test@example.com
+ATTENDEE;CN=3Dcn=3DGunnar Wrobel;PARTSTAT=3DACCEPTED:MAILTO:wrobel@example.o=
+rg
+DTSTAMP:20090418T194638Z
+END:VEVENT
+END:VCALENDAR
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.eml
new file mode 100644 (file)
index 0000000..b5f571d
--- /dev/null
@@ -0,0 +1,31 @@
+To: wrobel@example.org
+From: test@example.org
+Subject: Invitation kolab/issue3558
+Date: Fri, 21 Nov 2008 19:25:35 +0100
+MIME-Version: 1.0
+Content-Type: text/calendar;
+  method=request;
+  charset="utf-8"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN
+VERSION:2.0 
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTAMP:20090416T100820Z
+ORGANIZER;CN="Test":MAILTO:test@example.com
+ATTENDEE;CN="Test";RSVP=FALSE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:
+ mailto:test@example.com 
+ATTENDEE;RSVP=TRUE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:
+ wrobel@example.com
+CREATED:20090416T100818Z
+UID:libkcal-865928855.207
+LAST-MODIFIED:20090416T100818Z
+SUMMARY:issue3558
+DTSTART;VALUE=DATE:20090417
+DTEND;VALUE=DATE:20090418
+TRANSP:OPAQUE
+END:VEVENT 
+
+END:VCALENDAR
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/invitation_whole_day.ret
new file mode 100644 (file)
index 0000000..925afe4
--- /dev/null
@@ -0,0 +1,32 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: test@example.com
+Received: from localhost (localhost [127.0.0.1]) by localhost (Horde MIME library) with HTTP; Sat, 18 Apr 2009 21:46:38 +0200
+Message-ID: <20090418214638.lji09a8nk8ggg0os@localhost>
+Date: Sat, 18 Apr 2009 21:46:38 +0200
+From: cn=Gunnar Wrobel <wrobel@example.org>
+To: test@example.com
+Subject: Accepted: issue3558
+MIME-Version: 1.0
+Content-Type: text/calendar;
+       charset=UTF-8;
+       method="REPLY"
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+\r\n\r\nBEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//kolab.org//NONSGML Kolab Server 2//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:libkcal-865928855.207
+SUMMARY:issue3558
+DTSTART;VALUE=3DDATE:20090417
+DTEND;VALUE=3DDATE:20090418
+SEQUENCE:0
+ORGANIZER;CN=3DTest:MAILTO:test@example.com
+ATTENDEE;CN=3Dcn=3DGunnar Wrobel;PARTSTAT=3DACCEPTED:MAILTO:wrobel@example.o=
+rg
+DTSTAMP:20090418T194638Z
+END:VEVENT
+END:VCALENDAR
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.eml
new file mode 100644 (file)
index 0000000..5272bea
--- /dev/null
@@ -0,0 +1,29 @@
+To: wrobel@example.org
+From: test@example.org
+Subject: Invitation kolab/issue3236
+Date: Fri, 21 Nov 2008 19:25:35 +0100
+MIME-Version: 1.0
+Content-Type: text/calendar;
+  method=request;
+  charset="utf-8"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:wrobel@example.org
+ORGANIZER:MAILTO:test@example.org
+DTSTART:20081122T190000Z
+DTEND:20081122T193000Z
+SEQUENCE:0
+UID:040000008200E00074C5B7101A82E0080000000020D533ED0E4CC9010000000000000000100
+ 00000094C5C0A65E8CC4DB1AEC47FD1255FCD
+DTSTAMP:20081121T182534Z
+DESCRIPTION:Zeit: Samstag\, 22. November 2008 20:00-20:30 (GMT+01:00)
+  Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\,
+  Wien.\\n\\n*~*~*~*~*~*~*~*~*~*\\n\\n\\n
+SUMMARY:invitationtest2
+END:VEVENT
+END:VCALENDAR
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/longstring_invitation.ret
new file mode 100644 (file)
index 0000000..8fac58f
--- /dev/null
@@ -0,0 +1,36 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: test@example.org
+Received: from localhost (localhost [127.0.0.1]) by localhost (Horde MIME library) with HTTP; Sat, 18 Apr 2009 21:52:43 +0200
+Message-ID: <20090418215243.r10xkblym88oowss@localhost>
+Date: Sat, 18 Apr 2009 21:52:43 +0200
+From: cn=Gunnar Wrobel <wrobel@example.org>
+To: test@example.org
+Subject: Accepted: invitationtest2
+MIME-Version: 1.0
+Content-Type: text/calendar;
+       charset=UTF-8;
+       method="REPLY"
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+\r\n\r\nBEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//kolab.org//NONSGML Kolab Server 2//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:040000008200E00074C5B7101A82E0080000000020D533ED0E4CC901000000000000000
+ 010000000094C5C0A65E8CC4DB1AEC47FD1255FCD
+SUMMARY:invitationtest2
+DESCRIPTION:Zeit:
+  Samstag\, 22. November 2008 20:00-20:30 (GMT+01:00) Amsterdam\, Berlin\,
+  Bern\, Rom\, Stockholm\, Wien.\\\n\\\n*~*~*~*~*~*~*~*~*~*\\\n\\\n\\
+DTSTART:20081122T190000Z
+DTEND:20081122T193000Z
+SEQUENCE:0
+ORGANIZER:MAILTO:test@example.org
+ATTENDEE;CN=3Dcn=3DGunnar Wrobel;PARTSTAT=3DACCEPTED:MAILTO:wrobel@example.o=
+rg
+DTSTAMP:20090418T195243Z
+END:VEVENT
+END:VCALENDAR
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/null.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/null.ret
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/privileged.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/privileged.ret
new file mode 100644 (file)
index 0000000..41cb411
--- /dev/null
@@ -0,0 +1,17 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: me@example.org
+To: "You" <you@example.org>
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.eml
new file mode 100644 (file)
index 0000000..53cbf43
--- /dev/null
@@ -0,0 +1,202 @@
+Received: from localhost (example.org [127.0.0.1])
+       by example.org (Postfix) with ESMTP id 4E36AC2E001A
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at example.org
+X-Spam-Flag: NO
+X-Spam-Score: 3.704
+X-Spam-Level: ***
+X-Spam-Status: No, score=3.704 tagged_above=3 required=6.3 tests=[AWL=-1.351,
+       HTML_IMAGE_ONLY_32=2.353, HTML_MESSAGE=0.001, MIME_BASE64_TEXT=2.701]
+Received: from example.org ([127.0.0.1])
+       by localhost (example.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id 151vjdIDehPR for <wrobel@example.org>;
+       Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+Received: from localhost (example.org [127.0.0.1])
+       by example.org (Postfix) with ESMTP id 030C5C2E001C
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+Received: from ug-out-1516.google.com (ug-out-1516.google.com [66.249.92.165])
+       by example.org (Postfix) with ESMTP id D88AAC2E001A
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:07 +0200 (CEST)
+Received: by ug-out-1516.google.com with SMTP id 34so6700uge.3
+        for <wrobel@example.org>; Fri, 26 Sep 2008 01:54:07 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=google.com; s=beta;
+        h=domainkey-signature:mime-version:reply-to:sender:auto-submitted
+         :received:message-id:date:subject:from:to:content-type;
+        bh=NIbM6hZNgbcIAu3CGNdscvaw1JuMHR0cmVT+UvW5YdU=;
+        b=sGwxgD6sW0IDPFRSJcK9zvMtsS4jqziu+eF+p2fb7RsxCPFjU5FJFKbwhZQdVvA6I/
+         rzueuvNP0EqBhfsdLVyA==
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=google.com; s=beta;
+        h=mime-version:reply-to:sender:auto-submitted:message-id:date:subject
+         :from:to:content-type;
+        b=oo9wYH7lmDU5KDHTT4tpQ+BRlw/FYHBN2zt/an5FPuXE+dNM05UBVi8h87ig2c5Tvl
+         p53+hzDD2DoB/SlhqInA==
+MIME-Version: 1.0
+Reply-To: Gunnar Wrobel <test@example.org>
+Sender: Google Kalender <calendar-notification@google.com>
+Auto-Submitted: auto-generated
+Received: by 10.210.130.14 with SMTP id c14mr120659ebd.11.1222419247313; Fri, 
+       26 Sep 2008 01:54:07 -0700 (PDT)
+Message-ID: <0015174c3d6200b58f0457c8a9b5@google.com>
+Date: Fri, 26 Sep 2008 01:54:07 -0700
+Subject: =?ISO-8859-1?B?W0VpbmxhZHVuZ10gdHJnIEAgVORnbGljaCB1bSAxMTowMCAocmVzQGRldi5wYXJkdXMu?=
+       =?ISO-8859-1?B?ZGUp?=
+From: Gunnar Wrobel <test@example.org>
+To: "wrobel@example.org" <wrobel@example.org>
+Content-Type: multipart/mixed; boundary=0015174c3d6200b57b0457c8a97d
+
+--0015174c3d6200b57b0457c8a97d
+Content-Type: multipart/alternative; boundary=0015174c3d6200b5720457c8a97b
+
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
+Content-Transfer-Encoding: base64
+
+cmVzQGRldi5wYXJkdXMuZGUsIFNpZSB3dXJkZW4gZWluZ2VsYWRlbiB6dQ0KDQpUaXRlbDogdHJn
+DQpaZWl0OiBU5GdsaWNoIHVtIDExOjAwIChaZWl0em9uZTogUm9tKQ0KS2FsZW5kZXI6IHJlc0Bk
+ZXYucGFyZHVzLmRlDQoNClNpZSBr9m5uZW4gZGllc2VuIFRlcm1pbiBhbnplaWdlbiB1bnRlciAg
+DQpodHRwOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWJX
+czJiVzR6YUdGdWNEUTJhVGt3T0dWbGIyNHlaMmR1ZFdzZ2NtVnpRR1JsZGk1d1lYSmtkWE11WkdV
+JnRvaz1NaklqWjNkeWIySmxiRUJuYjI5bmJHVnRZV2xzTG1OdmJUQTNaVGc1Tm1aaFpqZGxZVEE0
+WmpjM1pqYzBNMk16T0dSak5ETXlaVGc0WkRFeU0yVXpNV1EmY3R6PUV1cm9wZSUyRlJvbWUmaGw9
+ZGUNCg0KDQoNCkRpZXNlIEUtTWFpbCB3dXJkZSBhbiByZXNAZGV2LnBhcmR1cy5kZSBnZXNlbmRl
+dCwgZGEgU2llIGVpbiBUZWlsbmVobWVyICANCmRpZXNlcyBUZXJtaW5zIHNpbmQuDQoNCkxlaG5l
+biBTaWUgZGllc2VuIFRlcm1pbiBhYiwgdW0ga2VpbmUgd2VpdGVyZW4gTmFjaHJpY2h0ZW4genUg
+ZGllc2VtIFRlcm1pbiAgDQp6dSBlcmhhbHRlbi4gU2llIGsmb3VtbDtubmVuIHNpY2ggYWx0ZXJu
+YXRpdiBmJnV1bWw7ciBlaW4gR29vZ2xlLUtvbnRvICANCnVudGVyIGh0dHA6Ly93d3cuZ29vZ2xl
+LmNvbS9jYWxlbmRhci8gYW5tZWxkZW4gdW5kIElocmUgIA0KQmVuYWNocmljaHRpZ3VuZ3NlaW5z
+dGVsbHVuZ2VuIGYmdXVtbDtyIElocmVuIGdlc2FtdGVuIEthbGVuZGVyIHN0ZXVlcm4uDQo=
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<div style="padding:10px 7px; font-size:12px; line-height:1.4 font-family:Arial,Sans-serif; text-align:center;"><div><a href="http://www.google.com/calendar/"><img style="border-width:0" src="http://www.google.com/calendar/images/blue_beta_de.gif" alt="Google Kalender"></a></div>
+<div style="width:370px; background:#D2E6D2; border-style:solid;       border-color:#ccc; border-width:1px 1px 0 1px; padding:15px 15px 5px 15px;       margin:0 auto"><p style="margin:0;color:#0">wrobel@example.org,
+Sie wurden eingeladen zu</p>
+<h2 style="margin:5px 0; font-size:18px; line-height:1.4;color:#0">trg</h2>
+<p style="margin:0 0 .5em;"><span style="color:#0">T&auml;glich um 11:00</span>
+<br>
+<span style="color:#676;">(Zeitzone:
+Rom)</span>
+<br>
+<span style="color:#0"></span>
+<br>
+<span style="color:#0">Kalender:
+wrobel@example.org</span></p>
+<p style="margin:0 0 1em;color:#0; white-space:pre-wrap !important; white-space:-moz-pre-wrap !important; white-space:-pre-wrap !important; white-space:-o-pre-wrap !important; white-space:pre; word-wrap:break-word;"><a href="http://www.google.com/calendar/event?action=VIEW&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Weitere Termindetails&raquo;</a></p>
+<div style="margin:.5em 0 0; text-align:center;color:#0"><strong>Nehmen Sie teil?</strong></div>
+<div style="margin:4px 0 0; text-align:center;"><span style="background:#fff; border:1px solid #676;              padding:3px 5px; line-height:1.5;"><a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=1&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Ja</a>
+|<a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=3&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Vielleicht</a>
+|<a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=2&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Nein</a></span></div></div>
+<div><img src="http://www.google.com/calendar/images/envelope.gif" style="background:#D2E6D2; width:420px height:95px" alt=""></div><p style="margin:-15px 0 0;">&nbsp;</p>
+<p style="color:#676;">Diese E-Mail wurde an wrobel@example.org gesendet, da Sie ein Teilnehmer dieses Termins sind.</p><p style="color:#676;">Lehnen Sie diesen Termin ab, um keine weiteren Nachrichten zu diesem Termin zu erhalten. Sie k&ouml;nnen sich alternativ f&uuml;r ein Google-Konto unter http://www.google.com/calendar/ anmelden und Ihre Benachrichtigungseinstellungen f&uuml;r Ihren gesamten Kalender steuern.</p></div>
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/calendar; charset=ISO-8859-1; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Rome
+X-LIC-LOCATION:Europe/Rome
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Rome:20080926T110000
+DTEND;TZID=Europe/Rome:20080926T120000
+RRULE:FREQ=DAILY;WKST=SU
+DTSTAMP:20080926T085407Z
+ORGANIZER;CN=Gunnar Wrobel:mailto:test@example.org
+UID:mk6mn3hanp46i908eeon2ggnuk@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=wrobel@example.org;X-NUM-GUESTS=0:mailto:wrobel@example.org
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=Gunnar Wrobel;X-NUM-GUESTS=0:mailto:test@example.org
+CLASS:PRIVATE
+CREATED:20080926T085406Z
+DESCRIPTION:Zeigen Sie Ihren Termin unter http://www.google.com/calendar/ev
+ ent?action=VIEW&eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXM
+ uZGU&tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOG
+ RjNDMyZTg4ZDEyM2UzMWQ&ctz=Europe%2FRome&hl=de an.
+LAST-MODIFIED:20080926T085406Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:trg
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0015174c3d6200b5720457c8a97b--
+--0015174c3d6200b57b0457c8a97d
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Rome
+X-LIC-LOCATION:Europe/Rome
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Rome:20080926T110000
+DTEND;TZID=Europe/Rome:20080926T120000
+RRULE:FREQ=DAILY;WKST=SU
+DTSTAMP:20080926T085407Z
+ORGANIZER;CN=Gunnar Wrobel:mailto:test@example.org
+UID:mk6mn3hanp46i908eeon2ggnuk@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=wrobel@example.org;X-NUM-GUESTS=0:mailto:wrobel@example.org
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=Gunnar Wrobel;X-NUM-GUESTS=0:mailto:test@example.org
+CLASS:PRIVATE
+CREATED:20080926T085406Z
+DESCRIPTION:Zeigen Sie Ihren Termin unter http://www.google.com/calendar/ev
+ ent?action=VIEW&eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXM
+ uZGU&tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOG
+ RjNDMyZTg4ZDEyM2UzMWQ&ctz=Europe%2FRome&hl=de an.
+LAST-MODIFIED:20080926T085406Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:trg
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0015174c3d6200b57b0457c8a97d--
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret
new file mode 100644 (file)
index 0000000..d1b1326
--- /dev/null
@@ -0,0 +1,206 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: else@example.org
+Received: from localhost (example.org [127.0.0.1])
+       by example.org (Postfix) with ESMTP id 4E36AC2E001A
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at example.org
+X-Spam-Flag: NO
+X-Spam-Score: 3.704
+X-Spam-Level: ***
+X-Spam-Status: No, score=3.704 tagged_above=3 required=6.3 tests=[AWL=-1.351,
+       HTML_IMAGE_ONLY_32=2.353, HTML_MESSAGE=0.001, MIME_BASE64_TEXT=2.701]
+Received: from example.org ([127.0.0.1])
+       by localhost (example.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id 151vjdIDehPR for <wrobel@example.org>;
+       Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+Received: from localhost (example.org [127.0.0.1])
+       by example.org (Postfix) with ESMTP id 030C5C2E001C
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:08 +0200 (CEST)
+Received: from ug-out-1516.google.com (ug-out-1516.google.com [66.249.92.165])
+       by example.org (Postfix) with ESMTP id D88AAC2E001A
+       for <wrobel@example.org>; Fri, 26 Sep 2008 10:54:07 +0200 (CEST)
+Received: by ug-out-1516.google.com with SMTP id 34so6700uge.3
+        for <wrobel@example.org>; Fri, 26 Sep 2008 01:54:07 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=google.com; s=beta;
+        h=domainkey-signature:mime-version:reply-to:sender:auto-submitted
+         :received:message-id:date:subject:from:to:content-type;
+        bh=NIbM6hZNgbcIAu3CGNdscvaw1JuMHR0cmVT+UvW5YdU=;
+        b=sGwxgD6sW0IDPFRSJcK9zvMtsS4jqziu+eF+p2fb7RsxCPFjU5FJFKbwhZQdVvA6I/
+         rzueuvNP0EqBhfsdLVyA==
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=google.com; s=beta;
+        h=mime-version:reply-to:sender:auto-submitted:message-id:date:subject
+         :from:to:content-type;
+        b=oo9wYH7lmDU5KDHTT4tpQ+BRlw/FYHBN2zt/an5FPuXE+dNM05UBVi8h87ig2c5Tvl
+         p53+hzDD2DoB/SlhqInA==
+MIME-Version: 1.0
+Reply-To: Gunnar Wrobel <test@example.org>
+Sender: Google Kalender <calendar-notification@google.com>
+Auto-Submitted: auto-generated
+Received: by 10.210.130.14 with SMTP id c14mr120659ebd.11.1222419247313; Fri, 
+       26 Sep 2008 01:54:07 -0700 (PDT)
+Message-ID: <0015174c3d6200b58f0457c8a9b5@google.com>
+Date: Fri, 26 Sep 2008 01:54:07 -0700
+Subject: =?ISO-8859-1?B?W0VpbmxhZHVuZ10gdHJnIEAgVORnbGljaCB1bSAxMTowMCAocmVzQGRldi5wYXJkdXMu?=
+       =?ISO-8859-1?B?ZGUp?=
+From: Gunnar Wrobel <test@example.org>
+To: "wrobel@example.org" <wrobel@example.org>
+Content-Type: multipart/mixed; boundary=0015174c3d6200b57b0457c8a97d
+X-Kolab-Scheduling-Message: TRUE
+
+--0015174c3d6200b57b0457c8a97d
+Content-Type: multipart/alternative; boundary=0015174c3d6200b5720457c8a97b
+
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
+Content-Transfer-Encoding: base64
+
+cmVzQGRldi5wYXJkdXMuZGUsIFNpZSB3dXJkZW4gZWluZ2VsYWRlbiB6dQ0KDQpUaXRlbDogdHJn
+DQpaZWl0OiBU5GdsaWNoIHVtIDExOjAwIChaZWl0em9uZTogUm9tKQ0KS2FsZW5kZXI6IHJlc0Bk
+ZXYucGFyZHVzLmRlDQoNClNpZSBr9m5uZW4gZGllc2VuIFRlcm1pbiBhbnplaWdlbiB1bnRlciAg
+DQpodHRwOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWJX
+czJiVzR6YUdGdWNEUTJhVGt3T0dWbGIyNHlaMmR1ZFdzZ2NtVnpRR1JsZGk1d1lYSmtkWE11WkdV
+JnRvaz1NaklqWjNkeWIySmxiRUJuYjI5bmJHVnRZV2xzTG1OdmJUQTNaVGc1Tm1aaFpqZGxZVEE0
+WmpjM1pqYzBNMk16T0dSak5ETXlaVGc0WkRFeU0yVXpNV1EmY3R6PUV1cm9wZSUyRlJvbWUmaGw9
+ZGUNCg0KDQoNCkRpZXNlIEUtTWFpbCB3dXJkZSBhbiByZXNAZGV2LnBhcmR1cy5kZSBnZXNlbmRl
+dCwgZGEgU2llIGVpbiBUZWlsbmVobWVyICANCmRpZXNlcyBUZXJtaW5zIHNpbmQuDQoNCkxlaG5l
+biBTaWUgZGllc2VuIFRlcm1pbiBhYiwgdW0ga2VpbmUgd2VpdGVyZW4gTmFjaHJpY2h0ZW4genUg
+ZGllc2VtIFRlcm1pbiAgDQp6dSBlcmhhbHRlbi4gU2llIGsmb3VtbDtubmVuIHNpY2ggYWx0ZXJu
+YXRpdiBmJnV1bWw7ciBlaW4gR29vZ2xlLUtvbnRvICANCnVudGVyIGh0dHA6Ly93d3cuZ29vZ2xl
+LmNvbS9jYWxlbmRhci8gYW5tZWxkZW4gdW5kIElocmUgIA0KQmVuYWNocmljaHRpZ3VuZ3NlaW5z
+dGVsbHVuZ2VuIGYmdXVtbDtyIElocmVuIGdlc2FtdGVuIEthbGVuZGVyIHN0ZXVlcm4uDQo=
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<div style="padding:10px 7px; font-size:12px; line-height:1.4 font-family:Arial,Sans-serif; text-align:center;"><div><a href="http://www.google.com/calendar/"><img style="border-width:0" src="http://www.google.com/calendar/images/blue_beta_de.gif" alt="Google Kalender"></a></div>
+<div style="width:370px; background:#D2E6D2; border-style:solid;       border-color:#ccc; border-width:1px 1px 0 1px; padding:15px 15px 5px 15px;       margin:0 auto"><p style="margin:0;color:#0">wrobel@example.org,
+Sie wurden eingeladen zu</p>
+<h2 style="margin:5px 0; font-size:18px; line-height:1.4;color:#0">trg</h2>
+<p style="margin:0 0 .5em;"><span style="color:#0">T&auml;glich um 11:00</span>
+<br>
+<span style="color:#676;">(Zeitzone:
+Rom)</span>
+<br>
+<span style="color:#0"></span>
+<br>
+<span style="color:#0">Kalender:
+wrobel@example.org</span></p>
+<p style="margin:0 0 1em;color:#0; white-space:pre-wrap !important; white-space:-moz-pre-wrap !important; white-space:-pre-wrap !important; white-space:-o-pre-wrap !important; white-space:pre; word-wrap:break-word;"><a href="http://www.google.com/calendar/event?action=VIEW&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Weitere Termindetails&raquo;</a></p>
+<div style="margin:.5em 0 0; text-align:center;color:#0"><strong>Nehmen Sie teil?</strong></div>
+<div style="margin:4px 0 0; text-align:center;"><span style="background:#fff; border:1px solid #676;              padding:3px 5px; line-height:1.5;"><a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=1&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Ja</a>
+|<a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=3&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Vielleicht</a>
+|<a href="http://www.google.com/calendar/event?action=RESPOND&amp;eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&amp;rst=2&amp;tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&amp;ctz=Europe%2FRome&amp;hl=de">Nein</a></span></div></div>
+<div><img src="http://www.google.com/calendar/images/envelope.gif" style="background:#D2E6D2; width:420px height:95px" alt=""></div><p style="margin:-15px 0 0;">&nbsp;</p>
+<p style="color:#676;">Diese E-Mail wurde an wrobel@example.org gesendet, da Sie ein Teilnehmer dieses Termins sind.</p><p style="color:#676;">Lehnen Sie diesen Termin ab, um keine weiteren Nachrichten zu diesem Termin zu erhalten. Sie k&ouml;nnen sich alternativ f&uuml;r ein Google-Konto unter http://www.google.com/calendar/ anmelden und Ihre Benachrichtigungseinstellungen f&uuml;r Ihren gesamten Kalender steuern.</p></div>
+--0015174c3d6200b5720457c8a97b
+Content-Type: text/calendar; charset=ISO-8859-1; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Rome
+X-LIC-LOCATION:Europe/Rome
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Rome:20080926T110000
+DTEND;TZID=Europe/Rome:20080926T120000
+RRULE:FREQ=DAILY;WKST=SU
+DTSTAMP:20080926T085407Z
+ORGANIZER;CN=Gunnar Wrobel:mailto:test@example.org
+UID:mk6mn3hanp46i908eeon2ggnuk@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=wrobel@example.org;X-NUM-GUESTS=0:mailto:wrobel@example.org
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=Gunnar Wrobel;X-NUM-GUESTS=0:mailto:test@example.org
+CLASS:PRIVATE
+CREATED:20080926T085406Z
+DESCRIPTION:Zeigen Sie Ihren Termin unter http://www.google.com/calendar/ev
+ ent?action=VIEW&eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXM
+ uZGU&tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOG
+ RjNDMyZTg4ZDEyM2UzMWQ&ctz=Europe%2FRome&hl=de an.
+LAST-MODIFIED:20080926T085406Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:trg
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0015174c3d6200b5720457c8a97b--
+--0015174c3d6200b57b0457c8a97d
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Rome
+X-LIC-LOCATION:Europe/Rome
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Rome:20080926T110000
+DTEND;TZID=Europe/Rome:20080926T120000
+RRULE:FREQ=DAILY;WKST=SU
+DTSTAMP:20080926T085407Z
+ORGANIZER;CN=Gunnar Wrobel:mailto:test@example.org
+UID:mk6mn3hanp46i908eeon2ggnuk@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=wrobel@example.org;X-NUM-GUESTS=0:mailto:wrobel@example.org
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=Gunnar Wrobel;X-NUM-GUESTS=0:mailto:test@example.org
+CLASS:PRIVATE
+CREATED:20080926T085406Z
+DESCRIPTION:Zeigen Sie Ihren Termin unter http://www.google.com/calendar/ev
+ ent?action=VIEW&eid=bWs2bW4zaGFucDQ2aTkwOGVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXM
+ uZGU&tok=MjIjZ3dyb2JlbEBnb29nbGVtYWlsLmNvbTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOG
+ RjNDMyZTg4ZDEyM2UzMWQ&ctz=Europe%2FRome&hl=de an.
+LAST-MODIFIED:20080926T085406Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:trg
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0015174c3d6200b57b0457c8a97d--
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret2 b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation.ret2
new file mode 100644 (file)
index 0000000..73cdd9f
--- /dev/null
@@ -0,0 +1,41 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: test@example.org
+Received: from localhost (localhost [127.0.0.1]) by localhost (Horde MIME library) with HTTP; Sat, 18 Apr 2009 21:55:07 +0200
+Message-ID: <20090418215507.2sldgjil1c0kk00g@localhost>
+Date: Sat, 18 Apr 2009 21:55:07 +0200
+From: cn=Gunnar Wrobel <wrobel@example.org>
+To: test@example.org
+Subject: Accepted: trg
+MIME-Version: 1.0
+Content-Type: text/calendar;
+       charset=UTF-8;
+       method="REPLY"
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+\r\n\r\nBEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//kolab.org//NONSGML Kolab Server 2//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:mk6mn3hanp46i908eeon2ggnuk@google.com
+SUMMARY:trg
+DESCRIPTION:Zeigen Sie Ihren Termin unter
+  http://www.google.com/calendar/event?action=3DVIEW&eid=3DbWs2bW4zaGFucDQ2a=
+TkwO
+ GVlb24yZ2dudWsgcmVzQGRldi5wYXJkdXMuZGU&tok=3DMjIjZ3dyb2JlbEBnb29nbGVtYWlsLm=
+Nv
+ bTA3ZTg5NmZhZjdlYTA4Zjc3Zjc0M2MzOGRjNDMyZTg4ZDEyM2UzMWQ&ctz=3DEurope%2FRome=
+&h
+ l=3Dde an.
+LOCATION:
+DTSTART;TZID=3DEurope/Rome:20080926T090000Z
+DTEND;TZID=3DEurope/Rome:20080926T100000Z
+SEQUENCE:0
+ORGANIZER;CN=3DGunnar Wrobel:mailto:test@example.org
+ATTENDEE;CN=3Dcn=3DGunnar Wrobel;PARTSTAT=3DACCEPTED:MAILTO:wrobel@example.o=
+rg
+DTSTAMP:20090418T195507Z
+END:VEVENT
+END:VCALENDAR
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation2.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/recur_invitation2.eml
new file mode 100644 (file)
index 0000000..3e5f8be
--- /dev/null
@@ -0,0 +1,86 @@
+Received: from localhost (example.com [127.0.0.1])
+       (Authenticated sender: 1@example.com)
+       by example.com (Postfix) with ESMTP id D47689B1C0
+       for <3@example.com>; Tue, 22 Sep 2009 15:36:22 +0200 (CEST)
+Message-ID:    <20090922153619.71582t7kl56avveo@example.com>
+Date: Tue, 22 Sep 2009 15:36:19 +0200
+From: 1 1 <1@example.com>
+To: 3@example.com
+Subject: 5
+User-Agent: Kronolith H3 (2.3)
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+       boundary="=_2qehldvr5f40"
+Content-Transfer-Encoding: 7bit
+
+This message is in MIME format.
+
+--=_2qehldvr5f40
+Content-Type: text/plain;
+       charset=UTF-8
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+5 (am 03.09.2009 um 06:00)
+
+Ort: 5
+
+Teilnehmer: 3@example.com
+
+Im Anhang befindet sich eine iCalendar-Datei mit mehr Informationen zu diese=
+m Termin. Wenn Ihr E-Mail-Programm iTip-Anfragen beherrscht, k=C3=B6nnen Sie=
+ diese Datei dazu benutzen, Ihre lokale Version des Termins zu aktualisieren=
+.
+
+Falls Ihr E-Mail-Programm keine iTip-Anfragen unterst=C3=BCtzt, k=C3=B6nnen =
+Sie einen der folgenden Links verwenden, um den Termin zu best=C3=A4tigen od=
+er abzulehnen.
+
+Um den Termin zu best=C3=A4tigen:
+https://example.com/client/kronolith/attend.php=
+?c=3D1%40example.com&e=3Dcee9e56161efb8b8831059=
+b2bc506e4e&u=3D3%40example.com&a=3Daccept
+
+Um den Termin unter Vorbehalt zu best=C3=A4tigen:
+https://example.com/client/kronolith/attend.php=
+?c=3D1%40example.com&e=3Dcee9e56161efb8b8831059=
+b2bc506e4e&u=3D3%40example.com&a=3Dtentative
+
+Um den Termin abzulehnen:
+https://example.com/client/kronolith/attend.php=
+?c=3D1%40example.com&e=3Dcee9e56161efb8b8831059=
+b2bc506e4e&u=3D3%40example.com&a=3Ddecline
+--=_2qehldvr5f40
+Content-Type: text/calendar;
+       charset=UTF-8;
+       name="event-invitation.ics";
+       METHOD="REQUEST"
+Content-Disposition: inline;
+       filename="event-invitation.ics"
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+X-WR-CALNAME:Kalender
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+BEGIN:VEVENT
+DTSTART:20090903T040000Z
+DTEND:20090903T050000Z
+DTSTAMP:20090922T133615Z
+UID:cee9e56161efb8b8831059b2bc506e4e
+CREATED:20090922T133619Z
+LAST-MODIFIED:20090922T133619Z
+SUMMARY:5
+ORGANIZER;CN=1 1:mailto:1@example.com
+LOCATION:5
+CLASS:PUBLIC
+STATUS:CONFIRMED
+TRANSP:OPAQUE
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:3@exam
+ ple.com
+RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TH
+END:VEVENT
+END:VCALENDAR
+
+--=_2qehldvr5f40--
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.eml
new file mode 100644 (file)
index 0000000..074413d
--- /dev/null
@@ -0,0 +1,105 @@
+Return-Path: <jramirezme@kof.org.mx>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id 395055FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:52 +0100 (CET)
+X-Quarantine-ID: <pcNZVfHFgssC>
+X-Virus-Scanned: by amavisd-new at demo.example.org
+X-Spam-Flag: YES
+X-Spam-Score: 15.576
+X-Spam-Level: ***************
+X-Spam-Status: Yes, score=15.576 tagged_above=3 required=6.3
+       tests=[FRT_PENIS1=3.799, HTML_MESSAGE=0.001, PLING_QUERY=2.16,
+       RCVD_FORGED_WROTE=4.365, RCVD_FORGED_WROTE2=2.052, RDNS_NONE=0.1,
+       SUBJECT_FUZZY_PENIS=3.099]
+Received: from demo.example.org ([127.0.0.1])
+       by localhost (demo.example.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id pcNZVfHFgssC for <wrobel@demo.example.org>;
+       Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id ABE8E5FC012A
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from town.windham.me.us (unknown [85.192.14.1])
+       by demo.example.org (Postfix) with SMTP id D10375FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:49 +0100 (CET)
+Received: from 192.85.78.105 (HELO ccfemsasmtp.kof.org.mx)
+     by demo.example.org with esmtp (CEYPNSAEHHT CYXPCJ)
+     id UNrWB0-GRSpeC-lz
+     for wrobel@demo.example.org; Sat, 10 Nov 2007 22:45:12 +0300
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: "Meredith F. Dick" <Meredith@kof.org.mx>
+To: "Marta Z. Rodrigues" <wrobel@demo.example.org>
+Subject: ***SPAM*** Don't you think it's time you stopped being a loser
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_7455_1D89_01C823EB.5AA2E210"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/plain;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+request=2E Horn was a fan favorite, standing up for theA recent poll by M=
+ontreal based newspaper Le Devoir, on
+Mel Karmazin spoke on behalf of both XM and Sirius=2E
+
+
+Do you believe in magic? We suppose you're likely to say "no"=2E=20
+We hadn't believed, either=2E=2E=2Euntil the moment MegaXXXX was introduc=
+ed!=20
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle!=20
+It's fabulous!
+
+So, hurry up, work a miracle in your life with this wonder-medicine!
+
+
+competiton has a poor track record of keeping down costsas head of the pu=
+ppet government in China=2EParti Qubcois leader slams radio host on homop=
+hobiacontrols=2E XM and Sirius can be said to compete with
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/html;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4=2E0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dus-ascii"=
+>
+<META content=3D"MSHTML 6=2E00=2E2900=2E2869" name=3DGENERATOR>
+<STYLE type=3D"text/css">
+=2Estyle2 {font-size: 10px; color: #616161;}
+=2Estyle7 {font-size: 14px; color: #FF2F2F;}
+body {background-color: #FFFFFF; color: #2B3235;}
+</STYLE>
+</HEAD>
+<BODY><span class=3D"style2">=20
+<br>request=2E Horn was a fan favorite, standing up for theA recent poll =
+by Montreal based newspaper Le Devoir, on<br>Mel Karmazin spoke on behalf=
+ of both XM and Sirius=2E</span>=20
+<br><br><br>
+<b>Do you believe in magic? We suppose you're likely to say "no"=2E<br>
+<span class=3D"style7">We hadn't believed, either=2E=2E=2Euntil the momen=
+t </span>MegaXXXX was introduced!</b><br>
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle! <br>
+<b>It's fabulous!</b><br><br>
+
+<a href=3D"http://scottjay=2Ecom/"><b>So, hurry up, work a miracle in you=
+r life with this wonder-medicine!</b></a><br><br>
+<br><span class=3D"style2">competiton has a poor track record of keeping =
+down costsas head of the puppet government in China=2EParti Qubcois leade=
+r slams radio host on homophobiacontrols=2E XM and Sirius can be said to =
+compete with<br></span></BODY></HTML>
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210--
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple.ret
new file mode 100644 (file)
index 0000000..3d2675e
--- /dev/null
@@ -0,0 +1,109 @@
+Mail from sender: me@example.com
+Mail to recipient: you@example.com
+Return-Path: <jramirezme@kof.com.mx>
+Received: from localhost (fqdn.example.com [127.0.0.1])
+        by demo.example.com (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from localhost (fqdn.example.com [127.0.0.1])
+       by demo.example.com (Postfix) with ESMTP id 395055FC0128
+       for <wrobel@demo.example.com>; Sat, 10 Nov 2007 20:44:52 +0100 (CET)
+X-Quarantine-ID: <pcNZVfHFgssC>
+X-Virus-Scanned: by amavisd-new at demo.example.com
+X-Spam-Flag: YES
+X-Spam-Score: 15.576
+X-Spam-Level: ***************
+X-Spam-Status: Yes, score=15.576 tagged_above=3 required=6.3
+       tests=[FRT_PENIS1=3.799, HTML_MESSAGE=0.001, PLING_QUERY=2.16,
+       RCVD_FORGED_WROTE=4.365, RCVD_FORGED_WROTE2=2.052, RDNS_NONE=0.1,
+       SUBJECT_FUZZY_PENIS=3.099]
+Received: from demo.example.com ([127.0.0.1])
+       by localhost (demo.example.com [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id pcNZVfHFgssC for <wrobel@demo.example.com>;
+       Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from localhost (fqdn.example.com [127.0.0.1])
+       by demo.example.com (Postfix) with ESMTP id ABE8E5FC012A
+       for <wrobel@demo.example.com>; Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from town.windham.me.us (unknown [85.192.14.1])
+       by demo.example.com (Postfix) with SMTP id D10375FC0128
+       for <wrobel@demo.example.com>; Sat, 10 Nov 2007 20:44:49 +0100 (CET)
+Received: from 192.85.78.105 (HELO ccfemsasmtp.kof.com.mx)
+     by demo.example.com with esmtp (CEYPNSAEHHT CYXPCJ)
+     id UNrWB0-GRSpeC-lz
+     for wrobel@demo.example.com; Sat, 10 Nov 2007 22:45:12 +0300
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: "Meredith F. Dick" <Meredith@kof.com.mx>
+To: "Marta Z. Rodrigues" <wrobel@demo.example.com>
+Subject: ***SPAM*** Don't you think it's time you stopped being a loser
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_7455_1D89_01C823EB.5AA2E210"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+X-Kolab-Scheduling-Message: FALSE
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/plain;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+request=2E Horn was a fan favorite, standing up for theA recent poll by M=
+ontreal based newspaper Le Devoir, on
+Mel Karmazin spoke on behalf of both XM and Sirius=2E
+
+
+Do you believe in magic? We suppose you're likely to say "no"=2E=20
+We hadn't believed, either=2E=2E=2Euntil the moment MegaXXXX was introduc=
+ed!=20
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle!=20
+It's fabulous!
+
+So, hurry up, work a miracle in your life with this wonder-medicine!
+
+
+competiton has a poor track record of keeping down costsas head of the pu=
+ppet government in China=2EParti Qubcois leader slams radio host on homop=
+hobiacontrols=2E XM and Sirius can be said to compete with
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/html;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4=2E0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dus-ascii"=
+>
+<META content=3D"MSHTML 6=2E00=2E2900=2E2869" name=3DGENERATOR>
+<STYLE type=3D"text/css">
+=2Estyle2 {font-size: 10px; color: #616161;}
+=2Estyle7 {font-size: 14px; color: #FF2F2F;}
+body {background-color: #FFFFFF; color: #2B3235;}
+</STYLE>
+</HEAD>
+<BODY><span class=3D"style2">=20
+<br>request=2E Horn was a fan favorite, standing up for theA recent poll =
+by Montreal based newspaper Le Devoir, on<br>Mel Karmazin spoke on behalf=
+ of both XM and Sirius=2E</span>=20
+<br><br><br>
+<b>Do you believe in magic? We suppose you're likely to say "no"=2E<br>
+<span class=3D"style7">We hadn't believed, either=2E=2E=2Euntil the momen=
+t </span>MegaXXXX was introduced!</b><br>
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle! <br>
+<b>It's fabulous!</b><br><br>
+
+<a href=3D"http://scottjay=2Ecom/"><b>So, hurry up, work a miracle in you=
+r life with this wonder-medicine!</b></a><br><br>
+<br><span class=3D"style2">competiton has a poor track record of keeping =
+down costsas head of the puppet government in China=2EParti Qubcois leade=
+r slams radio host on homophobiacontrols=2E XM and Sirius can be said to =
+compete with<br></span></BODY></HTML>
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210--
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple2.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple2.ret
new file mode 100644 (file)
index 0000000..90863cf
--- /dev/null
@@ -0,0 +1,109 @@
+Mail from sender: wrobel@example.org
+Mail to recipient: me@example.org
+Return-Path: <jramirezme@kof.org.mx>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id 395055FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:52 +0100 (CET)
+X-Quarantine-ID: <pcNZVfHFgssC>
+X-Virus-Scanned: by amavisd-new at demo.example.org
+X-Spam-Flag: YES
+X-Spam-Score: 15.576
+X-Spam-Level: ***************
+X-Spam-Status: Yes, score=15.576 tagged_above=3 required=6.3
+       tests=[FRT_PENIS1=3.799, HTML_MESSAGE=0.001, PLING_QUERY=2.16,
+       RCVD_FORGED_WROTE=4.365, RCVD_FORGED_WROTE2=2.052, RDNS_NONE=0.1,
+       SUBJECT_FUZZY_PENIS=3.099]
+Received: from demo.example.org ([127.0.0.1])
+       by localhost (demo.example.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id pcNZVfHFgssC for <wrobel@demo.example.org>;
+       Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id ABE8E5FC012A
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from town.windham.me.us (unknown [85.192.14.1])
+       by demo.example.org (Postfix) with SMTP id D10375FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:49 +0100 (CET)
+Received: from 192.85.78.105 (HELO ccfemsasmtp.kof.org.mx)
+     by demo.example.org with esmtp (CEYPNSAEHHT CYXPCJ)
+     id UNrWB0-GRSpeC-lz
+     for wrobel@demo.example.org; Sat, 10 Nov 2007 22:45:12 +0300
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: "Meredith F. Dick" <Meredith@kof.org.mx>
+To: "Marta Z. Rodrigues" <wrobel@demo.example.org>
+Subject: ***SPAM*** Don't you think it's time you stopped being a loser
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_7455_1D89_01C823EB.5AA2E210"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+X-Kolab-Scheduling-Message: FALSE
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/plain;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+request=2E Horn was a fan favorite, standing up for theA recent poll by M=
+ontreal based newspaper Le Devoir, on
+Mel Karmazin spoke on behalf of both XM and Sirius=2E
+
+
+Do you believe in magic? We suppose you're likely to say "no"=2E=20
+We hadn't believed, either=2E=2E=2Euntil the moment MegaXXXX was introduc=
+ed!=20
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle!=20
+It's fabulous!
+
+So, hurry up, work a miracle in your life with this wonder-medicine!
+
+
+competiton has a poor track record of keeping down costsas head of the pu=
+ppet government in China=2EParti Qubcois leader slams radio host on homop=
+hobiacontrols=2E XM and Sirius can be said to compete with
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/html;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4=2E0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dus-ascii"=
+>
+<META content=3D"MSHTML 6=2E00=2E2900=2E2869" name=3DGENERATOR>
+<STYLE type=3D"text/css">
+=2Estyle2 {font-size: 10px; color: #616161;}
+=2Estyle7 {font-size: 14px; color: #FF2F2F;}
+body {background-color: #FFFFFF; color: #2B3235;}
+</STYLE>
+</HEAD>
+<BODY><span class=3D"style2">=20
+<br>request=2E Horn was a fan favorite, standing up for theA recent poll =
+by Montreal based newspaper Le Devoir, on<br>Mel Karmazin spoke on behalf=
+ of both XM and Sirius=2E</span>=20
+<br><br><br>
+<b>Do you believe in magic? We suppose you're likely to say "no"=2E<br>
+<span class=3D"style7">We hadn't believed, either=2E=2E=2Euntil the momen=
+t </span>MegaXXXX was introduced!</b><br>
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle! <br>
+<b>It's fabulous!</b><br><br>
+
+<a href=3D"http://scottjay=2Ecom/"><b>So, hurry up, work a miracle in you=
+r life with this wonder-medicine!</b></a><br><br>
+<br><span class=3D"style2">competiton has a poor track record of keeping =
+down costsas head of the puppet government in China=2EParti Qubcois leade=
+r slams radio host on homophobiacontrols=2E XM and Sirius can be said to =
+compete with<br></span></BODY></HTML>
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210--
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple_out.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/simple_out.ret
new file mode 100644 (file)
index 0000000..41827a7
--- /dev/null
@@ -0,0 +1,108 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+Return-Path: <jramirezme@kof.org.mx>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id 395055FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:52 +0100 (CET)
+X-Quarantine-ID: <pcNZVfHFgssC>
+X-Virus-Scanned: by amavisd-new at demo.example.org
+X-Spam-Flag: YES
+X-Spam-Score: 15.576
+X-Spam-Level: ***************
+X-Spam-Status: Yes, score=15.576 tagged_above=3 required=6.3
+       tests=[FRT_PENIS1=3.799, HTML_MESSAGE=0.001, PLING_QUERY=2.16,
+       RCVD_FORGED_WROTE=4.365, RCVD_FORGED_WROTE2=2.052, RDNS_NONE=0.1,
+       SUBJECT_FUZZY_PENIS=3.099]
+Received: from demo.example.org ([127.0.0.1])
+       by localhost (demo.example.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id pcNZVfHFgssC for <wrobel@demo.example.org>;
+       Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from localhost (fqdn.example.org [127.0.0.1])
+       by demo.example.org (Postfix) with ESMTP id ABE8E5FC012A
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:51 +0100 (CET)
+Received: from town.windham.me.us (unknown [85.192.14.1])
+       by demo.example.org (Postfix) with SMTP id D10375FC0128
+       for <wrobel@demo.example.org>; Sat, 10 Nov 2007 20:44:49 +0100 (CET)
+Received: from 192.85.78.105 (HELO ccfemsasmtp.kof.org.mx)
+     by demo.example.org with esmtp (CEYPNSAEHHT CYXPCJ)
+     id UNrWB0-GRSpeC-lz
+     for wrobel@demo.example.org; Sat, 10 Nov 2007 22:45:12 +0300
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: "Meredith F. Dick" <Meredith@kof.org.mx>
+To: "Marta Z. Rodrigues" <wrobel@demo.example.org>
+Subject: ***SPAM*** Don't you think it's time you stopped being a loser
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+        boundary="----=_NextPart_7455_1D89_01C823EB.5AA2E210"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/plain;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+request=2E Horn was a fan favorite, standing up for theA recent poll by M=
+ontreal based newspaper Le Devoir, on
+Mel Karmazin spoke on behalf of both XM and Sirius=2E
+
+
+Do you believe in magic? We suppose you're likely to say "no"=2E=20
+We hadn't believed, either=2E=2E=2Euntil the moment MegaXXXX was introduc=
+ed!=20
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle!=20
+It's fabulous!
+
+So, hurry up, work a miracle in your life with this wonder-medicine!
+
+
+competiton has a poor track record of keeping down costsas head of the pu=
+ppet government in China=2EParti Qubcois leader slams radio host on homop=
+hobiacontrols=2E XM and Sirius can be said to compete with
+------=_NextPart_7455_1D89_01C823EB.5AA2E210
+Content-Type: text/html;
+        charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4=2E0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dus-ascii"=
+>
+<META content=3D"MSHTML 6=2E00=2E2900=2E2869" name=3DGENERATOR>
+<STYLE type=3D"text/css">
+=2Estyle2 {font-size: 10px; color: #616161;}
+=2Estyle7 {font-size: 14px; color: #FF2F2F;}
+body {background-color: #FFFFFF; color: #2B3235;}
+</STYLE>
+</HEAD>
+<BODY><span class=3D"style2">=20
+<br>request=2E Horn was a fan favorite, standing up for theA recent poll =
+by Montreal based newspaper Le Devoir, on<br>Mel Karmazin spoke on behalf=
+ of both XM and Sirius=2E</span>=20
+<br><br><br>
+<b>Do you believe in magic? We suppose you're likely to say "no"=2E<br>
+<span class=3D"style7">We hadn't believed, either=2E=2E=2Euntil the momen=
+t </span>MegaXXXX was introduced!</b><br>
+The effect this remedy produces on a male XXXXX cannot be called otherwis=
+e than a Miracle! <br>
+<b>It's fabulous!</b><br><br>
+
+<a href=3D"http://scottjay=2Ecom/"><b>So, hurry up, work a miracle in you=
+r life with this wonder-medicine!</b></a><br><br>
+<br><span class=3D"style2">competiton has a poor track record of keeping =
+down costsas head of the puppet government in China=2EParti Qubcois leade=
+r slams radio host on homophobiacontrols=2E XM and Sirius can be said to =
+compete with<br></span></BODY></HTML>
+
+------=_NextPart_7455_1D89_01C823EB.5AA2E210--
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/test.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/test.eml
new file mode 100644 (file)
index 0000000..b8d1024
--- /dev/null
@@ -0,0 +1,61 @@
+From: Test User <test@oberon.co.za>
+To: test2@oberon.co.za
+Subject: a
+Date: Wed, 19 May 2004 10:38:41 +0200
+User-Agent: KMail/1.6.52
+MIME-Version: 1.0
+Content-Type: Multipart/Mixed;
+  boundary="Boundary-00=_R0xqAO2ZmkKUNMZ"
+Message-Id: <200405191038.41626.test@oberon.co.za>
+Status: R
+X-Status: NQ
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+--Boundary-00=_R0xqAO2ZmkKUNMZ
+Content-Type: text/plain;
+  charset="us-ascii";
+  boundary=""
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+Organizer: test@oberon.co.za
+Summary: a
+Start Date: 2004-05-19
+Start Time: 12:00
+End Date: 2004-05-19
+End Time: 14:00
+
+--Boundary-00=_R0xqAO2ZmkKUNMZ
+Content-Type: text/calendar;
+  name="cal.ics";
+  method="request"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTAMP:20040519T083835Z
+ORGANIZER:MAILTO:test@oberon.co.za
+ATTENDEE;CN=Test2 User;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:test2@oberon.co.za
+CREATED:20040519T083734Z
+UID:libkcal-67797385.1001
+SEQUENCE:1
+LAST-MODIFIED:20040519T083734Z
+SUMMARY:a
+CLASS:PUBLIC
+PRIORITY:3
+DTSTART:20040614T080000Z
+DTEND:20040614T100000Z
+TRANSP:OPAQUE
+END:VEVENT
+
+END:VCALENDAR
+
+
+--Boundary-00=_R0xqAO2ZmkKUNMZ--
+
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.eml
new file mode 100644 (file)
index 0000000..d8d5557
--- /dev/null
@@ -0,0 +1,10 @@
+To: wrobel@pardus.example.org
+Subject: test
+From: Gunnar Wrobel <wrobel@kolab.example.org>
+Date: Tue, 27 Nov 2007 08:49:39 +0100
+Message-ID: <878x4k6sbw.fsf@kolab.example.org>
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/tiny.ret
new file mode 100644 (file)
index 0000000..375bdb4
--- /dev/null
@@ -0,0 +1,13 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.org
+To: wrobel@pardus.example.org
+Subject: test
+From: Gunnar Wrobel <wrobel@kolab.example.org>
+Date: Tue, 27 Nov 2007 08:49:39 +0100
+Message-ID: <878x4k6sbw.fsf@kolab.example.org>
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.eml
new file mode 100644 (file)
index 0000000..467a29b
--- /dev/null
@@ -0,0 +1,14 @@
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: me@example.org
+To: you@example.net
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+I'm on vacation
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/vacation.ret
new file mode 100644 (file)
index 0000000..f778aa7
--- /dev/null
@@ -0,0 +1,17 @@
+Mail from sender: me@example.org
+Mail to recipient: you@example.net
+Return-Path: <me@example.org>
+Received: from localhost (fqdn.example.org [127.0.0.1])
+        by demo.example.org (Cyrus v2.3.9-openpkg) with LMTPA;
+        Sat, 10 Nov 2007 20:44:52 +0100
+Message-ID: <1d2101c823d2$3555aa10$ac112c15@Meredith>
+From: me@example.org
+To: you@example.net
+Subject: Me to You
+Date: Sat, 10 Nov 2007 22:45:12 +0300
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+I'm on vacation
+.
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.eml b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.eml
new file mode 100644 (file)
index 0000000..cea849c
--- /dev/null
@@ -0,0 +1,10 @@
+To: %2$s
+Subject: test
+From: %1$s
+Date: Tue, 27 Nov 2007 08:49:39 +0100
+Message-ID: <878x4k6sbw.fsf@kolab.example.com>
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
diff --git a/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.ret b/framework/Kolab_Filter/test/Horde/Kolab/Filter/fixtures/validation.ret
new file mode 100644 (file)
index 0000000..dd84b76
--- /dev/null
@@ -0,0 +1,13 @@
+Mail from sender: %1$s
+Mail to recipient: %2$s
+To: %2$s
+Subject: test
+From: %1$s
+Date: Tue, 27 Nov 2007 08:49:39 +0100
+Message-ID: <878x4k6sbw.fsf@kolab.example.com>
+User-Agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/22.1.50 (x86_64-pc-linux-gnu)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test
+.
diff --git a/framework/LDAP/LDAP.php b/framework/LDAP/LDAP.php
new file mode 100644 (file)
index 0000000..11525aa
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * This is a utility class, every method is static.
+ *
+ * $Horde: framework/LDAP/LDAP.php,v 1.21 2009/10/04 03:03:45 chuck Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 2.2
+ * @package Horde_LDAP
+ */
+class Horde_LDAP
+{
+    /**
+     * Return a boolean expression using the specified operator.
+     *
+     * @param string $lhs    The attribute to test.
+     * @param string $op     The operator.
+     * @param string $rhs    The comparison value.
+     * @param array $params  Any additional parameters for the operator. @since
+     *                       Horde 3.2
+     *
+     * @return string  The LDAP search fragment.
+     */
+    public static function buildClause($lhs, $op, $rhs, $params = array())
+    {
+        switch ($op) {
+        case 'LIKE':
+            if (empty($rhs)) {
+                return '(' . $lhs . '=*)';
+            } elseif (!empty($params['begin'])) {
+                return sprintf('(|(%s=%s*)(%s=* %s*))', $lhs, Horde_LDAP::quote($rhs), $lhs, Horde_LDAP::quote($rhs));
+            } elseif (!empty($params['approximate'])) {
+                return sprintf('(%s=~%s)', $lhs, Horde_LDAP::quote($rhs));
+            }
+            return sprintf('(%s=*%s*)', $lhs, Horde_LDAP::quote($rhs));
+
+        default:
+            return sprintf('(%s%s%s)', $lhs, $op, Horde_LDAP::quote($rhs));
+        }
+    }
+
+    /**
+     * Escape characters with special meaning in LDAP searches.
+     *
+     * @param string $clause  The string to escape.
+     *
+     * @return string  The escaped string.
+     */
+    public static function quote($clause)
+    {
+        return str_replace(array('\\',   '(',  ')',  '*',  "\0"),
+                           array('\\5c', '\(', '\)', '\*', "\\00"),
+                           $clause);
+    }
+
+    /**
+     * Take an array of DN elements and properly quote it according to RFC
+     * 1485.
+     *
+     * @param array $parts  An array of tuples containing the attribute
+     *                      name and that attribute's value which make
+     *                      up the DN. Example:
+     *
+     *    $parts = array(0 => array('cn', 'John Smith'),
+     *                   1 => array('dc', 'example'),
+     *                   2 => array('dc', 'com'));
+     *
+     * @return string  The properly quoted string DN.
+     */
+    public static function quoteDN($parts)
+    {
+        $dn = '';
+        $count = count($parts);
+        for ($i = 0; $i < $count; $i++) {
+            if ($i > 0) {
+                $dn .= ',';
+            }
+            $dn .= $parts[$i][0] . '=';
+
+            // See if we need to quote the value.
+            if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $parts[$i][1])) {
+                $dn .= '"' . str_replace('"', '\\"', $parts[$i][1]) . '"';
+            } else {
+                $dn .= $parts[$i][1];
+            }
+        }
+
+        return $dn;
+    }
+}
diff --git a/framework/LDAP/package.xml b/framework/LDAP/package.xml
new file mode 100644 (file)
index 0000000..8de8a26
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_LDAP</name>
+ <channel>pear.horde.org</channel>
+ <summary>LDAP Utility Class</summary>
+ <description>Horde_LDAP:: contains some utility functions for dealing with LDAP
+servers and data.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>22:19:35</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Converted to package.xml 2.0 for pear.horde.org
+* Support approximate LDAP searches (requires openldap or another supported server).
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="tests">
+    <file name="quoteDN.phpt" role="test" />
+   </dir> <!-- //tests -->
+   <file baseinstalldir="/Horde" name="LDAP.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-12</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/LDAP/tests/quoteDN.phpt b/framework/LDAP/tests/quoteDN.phpt
new file mode 100644 (file)
index 0000000..631efa4
--- /dev/null
@@ -0,0 +1,44 @@
+--TEST--
+Horde_LDAP::quoteDN() tests
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../LDAP.php';
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'John Smith'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'Smith, John'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', ' John Smith'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'John Smith '),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'John  Smith'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'John+Smith'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+echo Horde_LDAP::quoteDN(array(array('cn', 'John "Bugsy" Smith'),
+                               array('dc', 'example'),
+                               array('dc', 'com'))) . "\n";
+
+?>
+--EXPECT--
+cn=John Smith,dc=example,dc=com
+cn="Smith, John",dc=example,dc=com
+cn=" John Smith",dc=example,dc=com
+cn="John Smith ",dc=example,dc=com
+cn="John  Smith",dc=example,dc=com
+cn="John+Smith",dc=example,dc=com
+cn="John \"Bugsy\" Smith",dc=example,dc=com
diff --git a/framework/Lens/lib/Horde/Lens.php b/framework/Lens/lib/Horde/Lens.php
new file mode 100644 (file)
index 0000000..7f9c380
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * $Horde: framework/Lens/lib/Horde/Lens.php,v 1.1 2008/03/05 20:37:26 chuck Exp $
+ *
+ * This set of classes implements a Flyweight pattern
+ * (http://en.wikipedia.org/wiki/Flyweight_pattern). Refactor/rename
+ * some based on this fact?
+ *
+ * @package Horde_Lens
+ */
+
+/**
+ * @package Horde_Lens
+ */
+class Horde_Lens implements Horde_Lens_Interface {
+
+    /**
+     */
+    protected $_target;
+
+    /**
+     */
+    public function decorate($target)
+    {
+        $this->_target = $target;
+        return $this;
+    }
+
+    /**
+     */
+    public function __get($key)
+    {
+        return $this->_target->$key;
+    }
+
+    /**
+     */
+    public function __set($key, $value)
+    {
+        $this->_target->$key = $value;
+        return $this;
+    }
+
+    /**
+     */
+    public function __call($func, $params)
+    {
+        return call_user_func_array(array($this->_target, $func), $params);
+    }
+
+}
diff --git a/framework/Lens/lib/Horde/Lens/Interface.php b/framework/Lens/lib/Horde/Lens/Interface.php
new file mode 100644 (file)
index 0000000..2f19d61
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * $Horde: framework/Lens/lib/Horde/Lens/Interface.php,v 1.1 2008/03/05 20:37:26 chuck Exp $
+ *
+ * This set of classes implements a Flyweight pattern
+ * (http://en.wikipedia.org/wiki/Flyweight_pattern). Refactor/rename
+ * some based on this fact?
+ *
+ * @package Horde_Lens
+ */
+
+/**
+ * @package Horde_Lens
+ */
+interface Horde_Lens_Interface {
+
+    /**
+     * Set the current object to view with the Lens.
+     */
+    public function decorate($target);
+
+}
diff --git a/framework/Lens/lib/Horde/Lens/Iterator.php b/framework/Lens/lib/Horde/Lens/Iterator.php
new file mode 100644 (file)
index 0000000..9ec3775
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * $Horde: framework/Lens/lib/Horde/Lens/Iterator.php,v 1.1 2008/03/05 20:37:26 chuck Exp $
+ *
+ * This set of classes implements a Flyweight pattern
+ * (http://en.wikipedia.org/wiki/Flyweight_pattern). Refactor/rename
+ * some based on this fact?
+ *
+ * @package Horde_Lens
+ */
+
+/**
+ */
+class Horde_Lens_Iterator implements OuterIterator {
+
+    /**
+     * The Iterator to decorate.
+     * @var Iterator
+     */
+    private $_i;
+
+    /**
+     * The Decorator that will observe each element of the iterator.
+     * @var Horde_Lens_Interface
+     */
+    protected $_d;
+
+    /**
+     * Constructs a decorator around an iterator using a single
+     * Horde_Lens_Interface object, which decorates the current()
+     * element of the iterator. The decorator is like a lens,
+     * decotrating one element at a time, instead of having a
+     * decorator for every element in the list.
+     *
+     * @param Iterator $i The iterator to decorate.
+     */
+    public function __construct(Iterator $i, $d = null)
+    {
+        $this->_i = $i;
+        if ($d !== null) {
+            $this->setLens($d);
+        }
+    }
+
+    /**
+     * Set or change the Lens modifying the inner iterator. Sets the
+     * current object of the lens automatically and returns the lens.
+     */
+    public function setLens(Horde_Lens_Interface $d)
+    {
+        $this->_d = $d;
+        return $this->current();
+    }
+
+    /**
+     * Rewind the inner iterator.
+     */
+    function rewind()
+    {
+        $this->_i->rewind();
+    }
+
+    /**
+     * Move to next element.
+     *
+     * @return void
+     */
+    function next()
+    {
+        $this->_i->next();
+    }
+
+    /**
+     * @return Whether more elements are available.
+     */
+    function valid()
+    {
+        return $this->_i->valid();
+    }
+
+    /**
+     * @return The current key.
+     */
+    function key()
+    {
+        return $this->_i->key();
+    }
+
+    /**
+     * @return The current value.
+     */
+    function current()
+    {
+        return $this->_d->decorate($this->_i->current());
+    }
+
+    /**
+     * @return Iterator The inner iterator.
+     */
+    function getInnerIterator()
+    {
+        return $this->_i;
+    }
+
+    /**
+     * Aggregate the inner iterator.
+     *
+     * @param func    Name of method to invoke.
+     * @param params  Array of parameters to pass to method.
+     */
+    function __call($func, $params)
+    {
+        return call_user_func_array(array($this->_i, $func), $params);
+    }
+
+}
diff --git a/framework/Lens/package.xml b/framework/Lens/package.xml
new file mode 100644 (file)
index 0000000..107a2e4
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Lens</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Lens decorating iterator/flyweight implementation</summary>
+ <description>This package provides a way of wrapping iterators to
+ avoid looping over them multiple times, using a single decorator
+ object (the &quot;lens&quot;) instead of creating one decorator for
+ every element.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2007-03-23</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial release.</notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Lens">
+      <file name="Interface.php" role="php" />
+      <file name="Iterator.php" role="php" />
+     </dir> <!-- /lib/Horde/Lens -->
+     <file name="Lens.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Lens/Interface.php" as="Horde/Lens/Interface.php" />
+   <install name="lib/Horde/Lens/Iterator.php" as="Horde/Lens/Iterator.php" />
+   <install name="lib/Horde/Lens.php" as="Horde/Lens.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Mobile/Mobile.php b/framework/Mobile/Mobile.php
new file mode 100644 (file)
index 0000000..961c4b9
--- /dev/null
@@ -0,0 +1,1386 @@
+<?php
+/**
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_card extends Horde_Mobile_element {
+
+    var $_name;
+    var $_title;
+    var $_form;
+    var $_elements = array();
+    var $_softkeys = array();
+    var $_linksetAdded = false;
+
+    /**
+     * Constructor
+     *
+     * @param string $name   The name of this card. Can be used in anchor
+     *                       links.
+     * @param string $title  If a string is provided here, it will be displayed
+     *                       in the HTML title bar, respectively somewhere on
+     *                       the WAP display. Using a title you will normally
+     *                       have to spend one of your few lines on your WAP
+     *                       display. Consider that some WAP phones/SDK's and
+     *                       handheld devices don't display the title at all.
+     */
+    function Horde_Mobile_card($name = null, $title = null)
+    {
+        $this->_name = $name;
+        $this->_title = $title;
+    }
+
+    function &add(&$element)
+    {
+        if (!is_a($element, 'Horde_Mobile_element')) {
+            $error = PEAR::raiseError('Invalid element.');
+            return $error;
+        } elseif (is_a($element, 'Horde_Mobile_text') ||
+                  is_a($element, 'Horde_Mobile_image') ||
+                  is_a($element, 'Horde_Mobile_link') ||
+                  is_a($element, 'Horde_Mobile_phone') ||
+                  is_a($element, 'Horde_Mobile_rule')) {
+            $block = new Horde_Mobile_block($element);
+            $this->_elements[] = &$block;
+        } elseif (is_a($element, 'Horde_Mobile_block')) {
+            $this->_elements[] = &$element;
+        } elseif (is_a($element, 'Horde_Mobile_form')) {
+            if (!empty($this->_form)) {
+                $error = PEAR::raiseError('Cards may only contain one Form element.');
+                return $error;
+            }
+            $this->_elements[] = &$element;
+            $this->_form = &$element;
+        } elseif (is_a($element, 'Horde_Mobile_linkset')) {
+            if ($this->_linksetAdded) {
+                $error = PEAR::raiseError('Cards may only contain one Linkset element.');
+                return $error;
+            }
+
+            $this->_elements[] = &$element;
+            $this->_linksetAdded = true;
+        } else {
+            $error = PEAR::raiseError('This element must be inside an appropriate container element.');
+            return $error;
+        }
+
+        return $element;
+    }
+
+    function softkey($url, $label)
+    {
+        $this->_softkeys[] = array('url' => $url,
+                                   'label' => $label);
+    }
+
+}
+
+/**
+ * Horde_Mobile::
+ *
+ * Horde API for generating Mobile content. Includes numerous utility
+ * functions, generalized element classes, and renderers for markup
+ * languages including WML, HDML, and CHTML.
+ *
+ * This class is the top level class of all Horde_Mobile classes. Your
+ * page should consist of exactly one Horde_Mobile object. Appropriate
+ * markup - Imode, WML, HDML, etc. - is generated by the appropriate
+ * renderer object
+ *
+ * Do not overstuff Horde_Mobile objects. Remember that a lot of WAP
+ * clients cannot handle more than about 1400 bytes of compiled data.
+ *
+ * Examples:
+ *
+ * $myPage = new Horde_Mobile();
+ * $myPage = new Horde_Mobile('My WAP page');
+ * $myPage = new Horde_Mobile('', 'center');
+ *
+ * // More stuff
+ *
+ * $myPage->add($myText);
+ *
+ * // More items
+ *
+ * $myPage->render();
+ *
+ * $Horde: framework/Mobile/Mobile.php,v 1.55 2009/10/09 22:07:13 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Mobile
+ */
+class Horde_Mobile extends Horde_Mobile_card {
+
+    var $_title;
+    var $_elements = array();
+    var $_cards = array();
+    var $_debug = false;
+
+    // Decide whether the simulator device is to be used. Only affects
+    // HTML browser output.
+    var $_simulator = false;
+
+    /**
+     * Constructor
+     *
+     * @param string $title  If a string is provided here, it will be displayed
+     *                       in the HTML title bar, respectively somewhere on
+     *                       the WAP display. Using a title you will normally
+     *                       have to spend one of your few lines on your WAP
+     *                       display. Consider that some WAP phones/SDK's and
+     *                       handheld devices don't display the title at all.
+     * @param string $agent  If specified, use instead of HTTP_USER_AGENT.
+     */
+    function Horde_Mobile($title = null, $agent = null)
+    {
+        if (!is_null($title)) {
+            $this->_title = $title;
+        }
+
+        require_once 'Horde/Browser.php';
+        $browser = &Horde_Browser::singleton($agent);
+
+        if ($browser->hasFeature('html')) {
+            $ml = 'html';
+        } elseif ($browser->hasFeature('wml')) {
+            $ml = 'wml';
+        } else {
+            $ml = 'html';
+        }
+
+        require_once dirname(__FILE__) . '/Mobile/Renderer.php';
+        $this->_renderer = &Horde_Mobile_Renderer::singleton($ml, $browser);
+    }
+
+    function &add(&$element)
+    {
+        if (is_a($element, 'Horde_Mobile_card')) {
+            if (count($this->_elements)) {
+                $error = PEAR::raiseError('You cannot mix Horde_Mobile_cards and other elements at the deck level.');
+                return $error;
+            }
+
+            $this->_usingCards = true;
+            $this->_cards[] = &$element;
+
+            return $element;
+        } else {
+            if (count($this->_cards)) {
+                $error = PEAR::raiseError('You cannot mix Horde_Mobile_cards and other elements at the deck level.');
+                return $error;
+            }
+
+            return parent::add($element);
+        }
+    }
+
+    /**
+     * Activates the built-in device simulator on bigscreen browsers.
+     * The device simulator is only fully-functional in Internet
+     * Explorer, because the layout requires a scrollable table
+     * element. Other browsers will fail to show content on pages
+     * longer than a single screen.
+     */
+    function useSimulator()
+    {
+        $this->_simulator = true;
+    }
+
+    /**
+     * Creates the page in the appropriate markup language. Depending
+     * on the renderer type, HTML (pure HTML, handheldfriendly AvantGo
+     * HTML, i-mode cHTML, MML), WML or HDML code is created.
+     */
+    function display()
+    {
+        $this->_renderer->render($this);
+    }
+
+}
+
+/**
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_element {
+
+    function get($attribute)
+    {
+        $attr = '_' . $attribute;
+        if (isset($this->$attr)) {
+            return $this->$attr;
+        } else {
+            return null;
+        }
+    }
+
+    function set($attribute, $value)
+    {
+        $attr = '_' . $attribute;
+        $this->$attr = $value;
+    }
+
+}
+
+/**
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_formElement extends Horde_Mobile_element {
+
+    var $_name;
+    var $_value;
+    var $_label;
+    var $_size;
+    var $_maxlength;
+    var $_type;
+    var $_format;
+    var $_mode;
+
+    /**
+     * Set input mode/istyle for japanese MML/i-mode devices.
+     *
+     * @param string $mode  Input mode, one of:
+     *                      'alpha' (default)
+     *                      'katakana'
+     *                      'hiragana'
+     *                      'numeric'
+     */
+    function setMode($mode)
+    {
+        $this->_mode = $mode;
+
+        // Map the mode into an appropriate format string, used for
+        // WML and HDML. If a format string was provided earlier, it
+        // will be overwritten.
+        switch ($mode) {
+        case 'hiragana':
+        case 'katakana':
+            $this->_format = '*M';
+            break;
+
+        case 'alpha':
+            $this->_format = '*m';
+            break;
+
+        case 'numeric':
+            $this->_format = '*N';
+            break;
+        }
+    }
+
+}
+
+/**
+ * This class defines a form with various possible input elements. The
+ * input elements have to be defined as separate objects and are
+ * linked to the form with a special "add" function. One Horde_Mobile
+ * object can contain only one Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myPage = new Horde_Mobile(...);
+ *
+ * $myForm = new Horde_Mobile_form("/mynextpage.wml");
+ * $myText = new Horde_Mobile_text(...);
+ * $myForm->add($myText);
+ * $myInput = new Horde_Mobile_input(...);
+ * $myForm->add($myInput);
+ * $mySubmit = new Horde_Mobile_submit(...);
+ * $myForm->add($mySubmit);
+ *
+ * $myPage->add($myForm);
+ *
+ * $myPage->render();
+ *
+ * @see Horde_Mobile_text
+ * @see Horde_Mobile_image
+ * @see Horde_Mobile_table
+ * @see Horde_Mobile_dl
+ * @see Horde_Mobile_input
+ * @see Horde_Mobile_textarea
+ * @see Horde_Mobile_select
+ * @see Horde_Mobile_radio
+ * @see Horde_Mobile_checkbox
+ * @see Horde_Mobile_submit
+ * @see Horde_Mobile_rule
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_form extends Horde_Mobile_element {
+
+    var $_url;
+    var $_method;
+    var $_elements = array();
+
+    /**
+     * Constructor
+     *
+     * @param string $url       Address where the user input is sent to.
+     * @param string $method    'post' (default) or 'get'.
+     * @param boolean $session  Preserve the sesion id in the form? Defaults to true.
+     */
+    function Horde_Mobile_form($url, $method = 'post', $session = true)
+    {
+        $this->_url = $url;
+        $this->_method = $method;
+
+        if ($session && !array_key_exists(session_name(), $_COOKIE)) {
+            $this->add(new Horde_Mobile_hidden(session_name(), session_id()));
+        }
+    }
+
+    function &add(&$formElement)
+    {
+        if (is_a($formElement, 'Horde_Mobile_submit')) {
+            $formElement->_form = &$this;
+            $block = new Horde_Mobile_block($formElement);
+            $this->_elements[] = &$block;
+        } elseif (is_a($formElement, 'Horde_Mobile_hidden') ||
+                  is_a($formElement, 'Horde_Mobile_block')) {
+            $this->_elements[] = &$formElement;
+        } elseif (is_a($formElement, 'Horde_Mobile_formElement') ||
+                  is_a($formElement, 'Horde_Mobile_text') ||
+                  is_a($formElement, 'Horde_Mobile_rule') ||
+                  is_a($formElement, 'Horde_Mobile_image')) {
+            $block = new Horde_Mobile_block($formElement);
+            $this->_elements[] = &$block;
+        } else {
+            $error = PEAR::raiseError('Specified element cannot be inside a form.');
+            return $error;
+        }
+
+        return $formElement;
+    }
+
+    function getDefaults()
+    {
+        $defaults = array();
+        foreach ($this->_elements as $val) {
+            switch (strtolower(get_class($val))) {
+            case 'horde_mobile_hidden':
+                $defaults[] = array('name' => $val->get('name'),
+                                    'value' => $val->get('value'),
+                                    'hidden' => true);
+                break;
+
+            case 'horde_mobile_block':
+                foreach ($val->_elements as $bval) {
+                    switch (strtolower(get_class($bval))) {
+                    case 'horde_mobile_checkbox':
+                        if ($bval->isChecked()) {
+                            $defaults[] = array('name' => $bval->get('name'),
+                                                'value' => $bval->get('value'));
+                        }
+                        break;
+
+                    case 'horde_mobile_input':
+                    case 'horde_mobile_textarea':
+                    case 'horde_mobile_select':
+                    case 'horde_mobile_radio':
+                        $defaults[] = array('name' => $bval->get('name'),
+                                            'value' => $bval->get('value'));
+                        break;
+
+                    case 'horde_mobile_hidden':
+                        $defaults[] = array('name' => $bval->get('name'),
+                                            'value' => $bval->get('value'),
+                                            'hidden' => true);
+                        break;
+
+                    case 'horde_mobile_table':
+                        foreach ($bval->_rows as $row) {
+                            foreach ($row->_columns as $col) {
+                                switch (strtolower(get_class($col))) {
+                                case 'horde_mobile_checkbox':
+                                    if ($col->isChecked()) {
+                                        $defaults[] = array('name' => $bval->get('name'),
+                                                            'value' => $bval->get('value'));
+                                    }
+                                    break;
+
+                                case 'horde_mobile_input':
+                                case 'horde_mobile_textarea':
+                                case 'horde_mobile_select':
+                                case 'horde_mobile_radio':
+                                    $defaults[] = array('name' => $bval->get('name'),
+                                                        'value' => $bval->get('value'));
+                                    break;
+
+                                case 'horde_mobile_hidden':
+                                    $defaults[] = array('name' => $bval->get('name'),
+                                                        'value' => $bval->get('value'),
+                                                        'hidden' => true);
+                                    break;
+                                }
+                            }
+                        }
+                        break;
+
+                    case 'horde_mobile_dl':
+                        foreach ($bval->_dts as $dt) {
+                            foreach ($dt->_dds as $dd) {
+                                switch (strtolower(get_class($dd))) {
+                                case 'horde_mobile_checkbox':
+                                    if ($dd->isChecked()) {
+                                        $defaults[] = array('name'  => $bval->get('name'),
+                                                            'value' => $bval->get('value'));
+                                    }
+                                    break;
+
+                                case 'horde_mobile_input':
+                                case 'horde_mobile_textarea':
+                                case 'horde_mobile_select':
+                                case 'horde_mobile_radio':
+                                    $defaults[] = array('name'  => $bval->get('name'),
+                                                        'value' => $bval->get('value'));
+                                    break;
+
+                                case 'horde_mobile_hidden':
+                                    $defaults[] = array('name'  => $bval->get('name'),
+                                                        'value' => $bval->get('value'));
+                                    break;
+                                }
+                            }
+                        }
+                        break;
+                    }
+                }
+                break;
+            }
+        }
+
+        return $defaults;
+    }
+
+    function getGetVars()
+    {
+        // Determine all elements that have to be submitted.
+        $getvars = array();
+        foreach ($this->_elements as $val) {
+            switch (strtolower(get_class($val))) {
+            case 'horde_mobile_block':
+                foreach ($val->_elements as $bval) {
+                    switch (strtolower(get_class($bval))) {
+                    case 'horde_mobile_input':
+                    case 'horde_mobile_hidden':
+                    case 'horde_mobile_textarea':
+                    case 'horde_mobile_select':
+                    case 'horde_mobile_checkbox':
+                    case 'horde_mobile_radio':
+                        $getvars[] = $bval->get('name');
+                        break;
+
+                    case 'horde_mobile_table':
+                        foreach ($bval->_rows as $row) {
+                            foreach ($row->_columns as $col) {
+                                switch (strtolower(get_class($col))) {
+                                case 'horde_mobile_input':
+                                case 'horde_mobile_hidden':
+                                case 'horde_mobile_textarea':
+                                case 'horde_mobile_select':
+                                case 'horde_mobile_checkbox':
+                                case 'horde_mobile_radio':
+                                    $getvars[] = $col->get('name');
+                                    break;
+                                }
+                            }
+                        }
+                        break;
+
+                    case 'horde_mobile_dl':
+                        foreach ($bval->_dts as $dt) {
+                            foreach ($dt->_dds as $dd) {
+                                switch (strtolower(get_class($dd))) {
+                                case 'horde_mobile_input':
+                                case 'horde_mobile_hidden':
+                                case 'horde_mobile_textarea':
+                                case 'horde_mobile_select':
+                                case 'horde_mobile_checkbox':
+                                case 'horde_mobile_radio':
+                                    $getvars[] = $dd->get('name');
+                                    break;
+                                }
+                            }
+                        }
+                        break;
+                    }
+                }
+                break;
+            }
+        }
+
+        return $getvars;
+    }
+
+}
+
+/**
+ * This class holds text-level elements for use in Horde_Mobile or
+ * Horde_Mobile_form objects.
+ *
+ * Examples:
+ *
+ * $block = new Horde_Mobile_block("Hello World");
+ * $text = new Horde_Mobile_text("Welcome to Horde_Mobile", 'b');
+ * $block->add($text);
+ *
+ * @see Horde_Mobile
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_block extends Horde_Mobile_element {
+
+    var $_elements = array();
+
+    /**
+     * Constructor.
+     *
+     * @param mixed $elements  Any elements (a single one or an array) to fill this block with.
+     */
+    function Horde_Mobile_block(&$elements)
+    {
+        if (!is_null($elements)) {
+            if (!is_array($elements)) {
+                $this->add($elements);
+            } else {
+                foreach ($elements as $element) {
+                    if ($this->allows($element)) {
+                        $this->_elements[] = $element;
+                    }
+                }
+            }
+        }
+    }
+
+    function &add(&$element)
+    {
+        if (!is_a($element, 'Horde_Mobile_element')) {
+            $error = PEAR::raiseError('Invalid element.');
+        } elseif ($this->allows($element)) {
+            $this->_elements[] = &$element;
+            return $element;
+        } else {
+            $error = PEAR::raiseError('The element is not allowed inside a block.');
+        }
+        return $error;
+    }
+
+    function allows($element)
+    {
+        return (is_a($element, 'Horde_Mobile_text') ||
+                is_a($element, 'Horde_Mobile_table') ||
+                is_a($element, 'Horde_Mobile_dl') ||
+                is_a($element, 'Horde_Mobile_image') ||
+                is_a($element, 'Horde_Mobile_formElement') ||
+                is_a($element, 'Horde_Mobile_link') ||
+                is_a($element, 'Horde_Mobile_linkset') ||
+                is_a($element, 'Horde_Mobile_phone') ||
+                is_a($element, 'Horde_Mobile_rule'));
+    }
+
+}
+
+/**
+ * This class inserts plain text into a Horde_Mobile_block or a
+ * Horde_Mobile_row object.
+ *
+ * Examples:
+ *
+ * $myText1 = new Horde_Mobile_text("Hello World");
+ * $myText2 = new Horde_Mobile_text("Welcome to Horde_Mobile", 'b');
+ * $myText3 = new Horde_Mobile_text("Good Morning", array('b', 'big'));
+ *
+ * @see Horde_Mobile_block
+ * @see Horde_Mobile_row
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_text extends Horde_Mobile_element {
+
+    var $_text = '';
+    var $_attributes = array();
+    var $_linebreaks = false;
+
+    /**
+     * Constructor
+     * @param string $text       The text content of the element.
+     * @param array $attributes  Text attributes. Any of:
+     *   'b'
+     *   'u'
+     *   'i'
+     *   'big'
+     *   'small'
+     */
+    function Horde_Mobile_text($text, $attributes = array())
+    {
+        $this->_text = $text;
+        if (!is_array($attributes)) {
+            $attributes = array($attributes);
+        }
+        $this->_attributes = $attributes;
+    }
+
+}
+
+/**
+ * This class allows to insert bitmap images into a Horde_Mobile_block,
+ * Horde_Mobile_form or Horde_Mobile_row object.
+ *
+ * Examples:
+ *
+ * $image = new Horde_Mobile_image('/path/to/image.wbmp',
+ *                                 array('height' => 100, 'width' => 100));
+ *
+ * @see Horde_Mobile_block
+ * @see Horde_Mobile_form
+ * @see Horde_Mobile_row
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_image extends Horde_Mobile_element {
+
+    var $_src = '';
+    var $_attributes = array();
+
+    /**
+     * Constructor
+     * @param string $src        The source location of the image.
+     * @param array $attributes  Image attributes. Any of:
+     *   'align'
+     *   'alt'
+     *   'height'
+     *   'hspace'
+     *   'vspace'
+     *   'width'
+     *   'class'
+     *   'id'
+     */
+    function Horde_Mobile_image($src, $attributes = array())
+    {
+        $this->_src = $src;
+        $this->_attributes = $attributes;
+    }
+
+}
+
+/**
+ * This class allows to insert tables into a Horde_Mobile or
+ * Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myTable = new Horde_Mobile_table();
+ *
+ * $row1 = new Horde_Mobile_row();
+ * $row1->add($image1);
+ * $row1->add($text1);
+ * $myTable->add($row1);
+ *
+ * $row2 = new Horde_Mobile_row();
+ * $row2->add($image2);
+ * $row2->add($text2);
+ * $myTable->add($row2);
+ *
+ * $myDeck->add($myTable);
+ *
+ * @see Horde_Mobile
+ * @see Horde_Mobile_form
+ * @see Horde_Mobile_row
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_table extends Horde_Mobile_block {
+
+    var $_rows = array();
+    var $_border = null;
+    var $_padding = null;
+    var $_spacing = null;
+
+    /**
+     * Adds a Horde_Mobile_row object to Horde_Mobile_table.
+     *
+     * @param Horde_Mobile_row $row  The row object to add.
+     */
+    function &add(&$row)
+    {
+        if (!is_a($row, 'Horde_Mobile_row')) {
+            $error = PEAR::raiseError('Rows must be Horde_Mobile_row objects.');
+            return $error;
+        }
+
+        $this->_rows[] = &$row;
+        return $row;
+    }
+
+}
+
+/**
+ * This class defines the rows that a Horde_Mobile_table object
+ * consists of.
+ *
+ * Examples:
+ *
+ * $image1 = new Horde_Mobile_image("my_image.wbmp", "my_image.png", ":-)");
+ * $text1 = new Horde_Mobile_text("my text");
+ * $row1 = new Horde_Mobile_row();
+ * $row1->add($image1);
+ * $row1->add();
+ * $row1->add($text1);
+ *
+ * @see Horde_Mobile_table
+ * @see Horde_Mobile_text
+ * @see Horde_Mobile_image
+ * @see Horde_Mobile_link
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_row extends Horde_Mobile_element {
+
+    var $_columns = array();
+
+    /**
+     * Adds a column element to a Horde_Mobile_row object.
+     *
+     * @param Horde_Mobile_element $cellElement  Can be a Horde_Mobile_text
+     *                                           object, a Horde_Mobile_image
+     *                                           object, a Horde_Mobile_link
+     *                                           object or null. The latter
+     *                                           results in an empty cell.
+     */
+    function &add($cellElement = null)
+    {
+        if (is_object($cellElement)) {
+            if (!is_a($cellElement, 'Horde_Mobile_text') &&
+                !is_a($cellElement, 'Horde_Mobile_link') &&
+                !is_a($cellElement, 'Horde_Mobile_image')) {
+                $error = PEAR::raiseError('Table cells can only contain text, links, or images.');
+                return $error;
+            }
+            $this->_columns[] = &$cellElement;
+            return $cellElement;
+        } elseif (!is_null($cellElement)) {
+            $t = new Horde_Mobile_text($cellElement);
+            $this->_columns[] = &$t;
+            return $t;
+        } else {
+            $this->_columns[] = &$cellElement;
+            return $cellElement;
+        }
+    }
+
+    function getColumnCount()
+    {
+        return count($this->_columns);
+    }
+
+}
+
+/**
+ * This class allows to insert definition lists into a Horde_Mobile or
+ * Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myDl = new Horde_Mobile_dl();
+ *
+ * $dt1 = new Horde_Mobile_dt();
+ * $dt1->add($image1);
+ * $dt1->add($text1);
+ * $myDl->add($dt1);
+ *
+ * $dt2 = new Horde_Mobile_dt();
+ * $dt2->add($image2);
+ * $dt2->add($text2);
+ * $myDl->add($dt2);
+ *
+ * $myDeck->add($myDl);
+ *
+ * @see Horde_Mobile
+ * @see Horde_Mobile_form
+ * @see Horde_Mobile_dt
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_dl extends Horde_Mobile_block {
+
+    var $_dts = array();
+
+    /**
+     * Adds a Horde_Mobile_dt object to Horde_Mobile_dl.
+     *
+     * @param Horde_Mobile_dt $dt The dl object to add.
+     */
+    function &add(&$dt)
+    {
+        if (!is_a($dt, 'Horde_Mobile_dt')) {
+            $error = PEAR::raiseError('Must be Horde_Mobile_dt objects.');
+            return $error;
+        }
+
+        $this->_dts[] = &$dt;
+        return $dt;
+    }
+
+}
+
+/**
+ * This class defines the terms of a Horde_Mobile_dl object.
+ *
+ * Examples:
+ *
+ * $image1 = new Horde_Mobile_image("my_image.wbmp", "my_image.png", ":-)");
+ * $text1 = new Horde_Mobile_text("my text");
+ * $dt1 = new Horde_Mobile_dt();
+ * $dt1->add($image1);
+ * $dt1->add();
+ * $dt1->add($text1);
+ *
+ * @see Horde_Mobile_dl
+ * @see Horde_Mobile_text
+ * @see Horde_Mobile_image
+ * @see Horde_Mobile_link
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_dt extends Horde_Mobile_element {
+
+    var $_dds = array();
+
+    /**
+     * Adds a definition term element to a Horde_Mobile_dt object.
+     *
+     * @param Horde_Mobile_Element $ddElement  Can be a Horde_Mobile_text
+     *                                         object, a Horde_Mobile_image
+     *                                         object, a Horde_Mobile_link
+     *                                         object or null. The latter
+     *                                         results in an empty dd.
+     *
+     * @return Horde_Mobile_Element
+     */
+    function &add($ddElement = null)
+    {
+        if (is_object($ddElement)) {
+            if (!is_a($ddElement, 'Horde_Mobile_text') &&
+                !is_a($ddElement, 'Horde_Mobile_link') &&
+                !is_a($ddElement, 'Horde_Mobile_image')) {
+                $error = PEAR::raiseError('Description can only contain text, links or images.');
+                return $error;
+            }
+            $this->_dds[]= &$ddElement;
+            return $ddElement;
+        } elseif (!is_null($ddElement)) {
+            $t = new Horde_Mobile_text($ddElement);
+            $this->_dds[] = &$t;
+            return $t;
+        } else {
+            $this->_dds[] = &$ddElement;
+            return $ddElement;
+        }
+    }
+
+    function getDdsCount()
+    {
+        return count($this->_dds);
+    }
+
+}
+
+/**
+ * This class provides a text input field in a Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myInput1 = new Horde_Mobile_input('cid', '', 'Customer ID');
+ *
+ * $myInput2 = new Horde_Mobile_input('cid', '', 'Customer ID', '*N');
+ * $myInput2->set_size(6);
+ * $myInput2->set_maxlength(6);
+ *
+ * $myInput3 = new Horde_Mobile_input('pw', '', 'Password', '*N');
+ * $myInput3->set_size(8);
+ * $myInput3->set_maxlength(8);
+ * $myInput3->set_type('password');
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_input extends Horde_Mobile_formElement {
+
+    /**
+     * Constructor
+     *
+     * @param string $name    Variable in which the input is sent to the
+     *                        destination URL.
+     * @param string $value   Initial value that will be presented in the
+     *                        input field.
+     * @param string $label   Describes your input field on the surfer's
+     *                        screen/display.
+     * @param string $format  Input format code according to the WAP standard.
+     *                        Allows the WAP user client e.g. to input only
+     *                        digits and no characters. On an HTML generated
+     *                        page this format has no significance.
+     */
+    function Horde_Mobile_input($name, $value, $label = '', $format = '*M')
+    {
+        $this->_name = $name;
+        $this->_value = $value;
+        $this->_label = $label;
+        $this->_format = $format;
+        $this->_type = 'text';
+        $this->_mode = 'alpha';
+    }
+
+}
+
+/**
+ * This class provides an input textarea in a Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myArea1 = new Horde_Mobile_textarea('fb', '', 'Feedback');
+ * $myArea2 = new Horde_Mobile_textarea('msg', 'Enter message here ...', 'Message', 40, 5);
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_textarea extends Horde_Mobile_formElement {
+
+    var $_rows;
+    var $_cols;
+
+    /**
+     * Constructor.
+     *
+     * @param string $name   Variable in which the input is sent to the
+     *                       destination URL.
+     * @param string $value  Initial value that will be presented in the
+     *                       textarea.
+     * @param string $label  Describes your textarea on the surfer's
+     *                       screen/display.
+     * @param integer $rows  Rows.
+     * @param integer $cols  Columns.
+     */
+    function Horde_Mobile_textarea($name, $value, $label, $rows = 3, $cols = 16)
+    {
+        $this->_name = $name;
+        $this->_value = $value;
+        $this->_label = $label;
+        $this->_rows = $rows;
+        $this->_cols = $cols;
+        $this->_mode = 'alpha';
+    }
+
+}
+
+/**
+ * This class provides a select element in a Horde_Mobile_form object.
+ * It allows to create optimized WML for WAP devices which are capable
+ * to interprete the Openwave GUI extensions for WML 1.3. All other
+ * WML devices receive WML 1.1 compatible markup code, which is quite
+ * similar to the markup code created by the Horde_Mobile_radio class.
+ *
+ * Examples:
+ *
+ * $mySelect = new Horde_Mobile_select('color');
+ * $mySelect->add('Blue', 'b');
+ * $mySelect->add('Red', 'r', true);
+ * $mySelect->add('Yellow', 'y');
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_select extends Horde_Mobile_formElement {
+
+    var $_type;
+    var $_options = array();
+    var $_htmlchars = true;
+
+    /**
+     * Constructor
+     *
+     * @param string $name  Variable in which the information about the
+     *                      selected option is sent to the destination URL.
+     * @param string $type  Type of select area:
+     *                      'popup': popup the whole selection list
+     *                      'spin':  rotate options on a WAP device screen (OW
+     *                               1.3 GUI only).
+     * @param string $label Describes your input field on the surfer's
+     *                      screen/display.
+     *
+     * @param string $htmlchars Are the options already encoded for output?
+     */
+    function Horde_Mobile_select($name, $type = 'popup', $label = '', $htmlchars = false)
+    {
+        $this->_name = $name;
+        $this->_type = $type;
+        $this->_label = $label;
+        $this->_value = null;
+        $this->_htmlchars = $htmlchars;
+    }
+
+    /**
+     * Adds one option to a Horde_Mobile_select object.
+     *
+     * @param string $label         Describes the option on the surfer's
+     *                              screen/display.
+     * @param string $value         Value sent in the "name" variable, if this
+     *                              is the option selected.
+     * @param boolean $is_selected  Allowed values are true or false.
+     */
+    function add($label, $value, $is_selected = false)
+    {
+        $this->_options[] = array('label' => $label,
+                                  'value' => $value);
+
+        if (is_null($this->_value) || $is_selected) {
+            $this->_value = $value;
+        }
+    }
+
+}
+
+/**
+ * This class provides a radio button element in a Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myRadio = new Horde_Mobile_radio('country');
+ * $myRadio->add('Finland', 'F');
+ * $myRadio->add('Germany', 'G', true);
+ * $myRadio->add('Sweden', 'S');
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_radio extends Horde_Mobile_formElement {
+
+    var $_buttons = array();
+
+    /**
+     * Constructor
+     *
+     * @param string $name  Variable in which the information about the pressed button
+     *                      is sent to the destination URL.
+     */
+    function Horde_Mobile_radio($name)
+    {
+        $this->_name = $name;
+        $this->_value = null;
+    }
+
+    /**
+     * Adds one radio button to a Horde_Mobile_radio object.
+     *
+     * @param string $label        Describes the radiobutton on the surfer's
+     *                             screen/display.
+     * @param string $value        Value sent in the "name" variable, if this
+     *                             button is selected.
+     * @param boolean $is_checked  Allowed values are true or false.
+     */
+    function add($label, $value, $is_checked = false)
+    {
+        $this->_buttons[] = array('label' => $label,
+                                  'value' => $value);
+
+        if (!$this->_value || ($is_checked)) {
+            $this->_value = $value;
+        }
+    }
+
+}
+
+/**
+ * This class provides a single checkbox element in a Horde_Mobile_form object.
+ *
+ * Examples:
+ *
+ * $myCheckbox = new Horde_Mobile_checkbox('agmt', 'yes', 'I agree');
+ * $myCheckbox = new Horde_Mobile_checkbox('agmt', 'yes', 'I agree', false);
+ * $myCheckbox = new Horde_Mobile_checkbox('agmt', 'yes', 'I agree', true);
+ *
+ * @note The first and second examples are identical.
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_checkbox extends Horde_Mobile_formElement {
+
+    var $_checked;
+
+    /**
+     * Constructor
+     *
+     * @param string $name      Variable in which "value" is sent to the
+     *                          destination URL, if the box is checked.
+     * @param string $value     See name.
+     * @param string $label     Describes the checkbox on the surfer's
+     *                          screen/display.
+     * @param boolean $checked  Allowed values are true or false.
+     */
+    function Horde_Mobile_checkbox($name, $value, $label, $checked = false)
+    {
+        $this->_name  = $name;
+        $this->_value = $value;
+        $this->_label = $label;
+        $this->_checked = $checked;
+    }
+
+    function isChecked()
+    {
+        return $this->_checked;
+    }
+
+}
+
+/**
+ * This class provides hidden elements in Horde_Mobile_form objects.
+ *
+ * Examples:
+ *
+ * $hidden = new Horde_Mobile_hidden('internal_reference', '08154711');
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_hidden extends Horde_Mobile_formElement {
+
+    /**
+     * Constructor
+     *
+     * @param string $name   Variable in which $value is sent to the destination URL.
+     * @param string $value  See name.
+     */
+    function Horde_Mobile_hidden($name, $value)
+    {
+        $this->_name = $name;
+        $this->_value = $value;
+    }
+
+}
+
+/**
+ * This class provides a submit button for a Horde_Mobile_form object. One
+ * Horde_Mobile_form object can contain only one Horde_Mobile_submit object.
+ *
+ * Examples:
+ * $mySubmit = new Horde_Mobile_submit('Submit');
+ * $mySubmit = new Horde_Mobile_submit('Submit', 'user_pressed');
+ *
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_submit extends Horde_Mobile_formElement {
+
+    var $_form;
+
+    /**
+     * Constructor
+     *
+     * @param string $label  What's written on the button.
+     * @param string $name   Variable in which "label" is sent to the
+     *                       destination URL.
+     */
+    function Horde_Mobile_submit($label, $name = '')
+    {
+        $this->_label = $label;
+        $this->_name = $name;
+    }
+
+}
+
+/**
+ * This class provides a link in a Horde_Mobile, Horde_Mobile_linkset or
+ * Horde_Mobile_table object.
+ *
+ * Examples:
+ *
+ * $myPage = new Horde_Mobile(...);
+ *
+ * $myLink = new Horde_Mobile_link('Continue', '/mynextpage.wml');
+ * $myPage->add($myLink);
+ *
+ * @see Horde_Mobile
+ * @see Horde_Mobile_linkset
+ * @see Horde_Mobile_table
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_link extends Horde_Mobile_element {
+
+    var $_label;
+    var $_url;
+    var $_title;
+    var $_accesskey;
+
+    /**
+     * Constructor
+     *
+     * @param string $label  Describes the link on the surfer's screen/display.
+     * @param string $url    Next destination address. MUST be valid XML (&amp; instead of &, etc.).
+     * @param string $title  If a string is provided here, it will be displayed
+     *                       in the HTML browser status bar during
+     *                       "MouseOver", respectively somewhere on the WAP
+     *                       display. In order to work well with a broad range
+     *                       of user agents, keep your title under 6
+     *                       characters.
+     */
+    function Horde_Mobile_link($label, $url, $title = '')
+    {
+        $this->_label = $label;
+        $this->_url = $url;
+        $this->_title = $title;
+
+        // No accesskey assigned by default; can be assigned later
+        // from a Horde_Mobile_linkset object if required.
+        $this->_accesskey = null;
+    }
+
+}
+
+/**
+ * This class provides a phone number in a Horde_Mobile object. If supported by
+ * their mobile device, users can establish a voice connection to the
+ * specified number.
+ *
+ * Examples:
+ *
+ * $myPhone = &new Horde_Mobile_phone('123-45678', 'CALL');
+ * $myPage->add($myPhone);
+ *
+ * @see Horde_Mobile
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_phone extends Horde_Mobile_element {
+
+    var $_label;
+    var $_number;
+    var $_title;
+
+    /**
+     * Constructor
+     *
+     * @param string $phone_number  Phone number to dial.
+     * @param string $title         If a string is provided here, the call
+     *                              button on a WAP/HDML device will be
+     *                              enabled. In order to work well with
+     *                              a broad range of user agents, keep your
+     *                              title under 6 characters.
+     */
+    function Horde_Mobile_phone($phone_number, $title = '')
+    {
+        $this->_label = $phone_number;
+        $this->_number = preg_replace('|\D|', '', $phone_number);
+        $this->_title = $title;
+    }
+
+}
+
+/**
+ * This class defines a set of links. The links have to be defined as
+ * separate Horde_Mobile_link objects and are attached to the linkset
+ * with a special "add" function.  For WAP devices browser-dependent
+ * WML code will be created. On all UP-browser-based WAP devices
+ * linksets allow easier navigation through WML decks by using the
+ * "onpick" WML option and therefore are improving the "usability" of
+ * an application. Instead of painfully navigating through the links
+ * "sports->football->results->today" the mobile user e.g. can press
+ * "2431" on the keypad to enter his favorite deck. For all other WAP
+ * devices normal <a> tags are created. One Horde_Mobile object can
+ * contain only one linkset object.
+ *
+ * Examples:
+ *
+ * $myPage = new Horde_Mobile(...);
+ *
+ * $myLinkset = new Horde_Mobile_linkset();
+ * $myLink1 = new Horde_Mobile_link("Phonebook", "/wap/phonebook.wml");
+ * $myLinkset->add($myLink1);
+ * $myLink2 = new Horde_Mobile_link("DateBook", "/wap/datebook.wml");
+ * $myLinkset->add($myLink2);
+ *
+ * $myPage->add($myLinkset);
+ *
+ * $myPage->render();
+ *
+ * @see Horde_Mobile_link
+ * @see Horde_Mobile
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_linkset extends Horde_Mobile_element {
+
+    var $_elements;
+
+    /**
+     * Adds a Horde_Mobile_link object to Horde_Mobile_linkset.
+     *
+     * @param Horde_Mobile_link $link  The link object to add.
+     *
+     * @see Horde_Mobile_link
+     */
+    function &add(&$link)
+    {
+        if (!is_a($link, 'Horde_Mobile_link')) {
+            $error = PEAR::raiseError('Links must be Horde_Mobile_link objects.');
+            return $error;
+        }
+
+        $this->_elements[] = &$link;
+        $link->set('accesskey', count($this->_elements));
+
+        return $link;
+    }
+
+}
+
+/**
+ * This class will cause a horizontal rule to be drawn across the screen.  You
+ * can use it to separate text paragraphs in Horde_Mobile or Horde_Mobile_form
+ * objects.
+ *
+ * Examples:
+ *
+ * $myDefaultRule = new Horde_Mobile_rule();
+ * $mySpecialRule = new Horde_Mobile_rule('60%', 4);
+ *
+ * $myPage->add($myDefaultRule);
+ *
+ * $myPage->add($mySpecialRule);
+ *
+ * @see Horde_Mobile
+ * @see Horde_Mobile_form
+ *
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_rule extends Horde_Mobile_element {
+
+    var $_width;
+    var $_size;
+
+    /**
+     * Constructor
+     *
+     * @param integer $width  Percentage of screen width or absolute value in
+     *                        number of pixels (e.g. "50%", 100).
+     * @param integer $size   Height of the line to be drawn in pixels.
+     */
+    function Horde_Mobile_rule($width = '', $size = '')
+    {
+        $this->_width = $width;
+        $this->_size = $size;
+    }
+
+}
diff --git a/framework/Mobile/Mobile/Renderer.php b/framework/Mobile/Mobile/Renderer.php
new file mode 100644 (file)
index 0000000..7075316
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+/**
+ * Horde_Mobile_Renderer:: framework for mobile device markup
+ * renderers.
+ *
+ * $Horde: framework/Mobile/Mobile/Renderer.php,v 1.29 2009/01/06 17:49:33 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_Renderer extends Horde_Mobile {
+
+    var $_browser;
+
+    function Horde_Mobile_Renderer($browser = null)
+    {
+        if (is_null($browser)) {
+            $this->_browser = new Browser();
+        } else {
+            $this->_browser = $browser;
+        }
+    }
+
+    function isBrowser($agent)
+    {
+        return $this->_browser->isBrowser($agent);
+    }
+
+    function hasQuirk($quirk)
+    {
+        return $this->_browser->hasQuirk($quirk);
+    }
+
+    /**
+     * Render any Horde_Mobile_element object. Looks for the
+     * appropriate rendering function in the renderer; if there isn't
+     * one, we ignore this element.
+     *
+     * @param Horde_Mobile_element $element  The element to render.
+     */
+    function renderElement(&$element)
+    {
+        $func = '_render' . ucfirst(str_replace('horde_mobile_', '', strtolower(get_class($element))));
+        if (method_exists($this, $func)) {
+            $this->$func($element);
+        }
+    }
+
+    function _renderBlock(&$block)
+    {
+        if (count($block->_elements)) {
+            echo '<p>';
+            foreach ($block->_elements as $blockElement) {
+                $this->renderElement($blockElement);
+            }
+            echo "</p>\n";
+        }
+    }
+
+    function _renderForm(&$form)
+    {
+        foreach ($form->_elements as $formElement) {
+            $this->renderElement($formElement);
+        }
+    }
+
+    function _renderDl(&$dl)
+    {
+        foreach ($dl->_dts as $dt) {
+            $this->_renderDt($dt);
+        }
+    }
+
+    function _renderDt(&$dt)
+    {
+        $i = 0;
+        foreach ($dt->_dds as $dd) {
+            echo $out = ($i == 0) ? '<dt>' : '<dd>';
+
+            // Call create function for each ddelement that is a
+            // Horde_Mobile object.
+            if (!is_null($dd)) {
+                $this->renderElement($dd);
+            }
+
+            echo $out = ($i++ == 0) ? '</dt>' : '</dd>';
+        }
+    }
+
+    function _renderTable(&$table)
+    {
+        foreach ($table->_rows as $row) {
+            $this->_renderRow($row);
+        }
+    }
+
+    function _renderRow(&$row)
+    {
+        echo '<tr>';
+        foreach ($row->_columns as $column) {
+            echo '<td>';
+            // Call create function for each cellelement that is a
+            // Horde_Mobile object.
+            if (!is_null($column)) {
+                $this->renderElement($column);
+            }
+            echo '</td>';
+        }
+        echo "</tr>\n";
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Mobile_Renderer instance
+     * based on $type.
+     *
+     * @param string $type      The kind of markup (html, hdml, wml) we want to
+     *                          generate.
+     * @param Browser $browser  The Browser object to use.
+     * @param array $params     A hash containing any options for the renderer.
+     *
+     * @return Horde_Mobile_Renderer  The newly created concrete
+     *                                Horde_Mobile_Renderer instance, or a
+     *                                PEAR_Error object on an error.
+     */
+    function &factory($type, $browser = null, $params = array())
+    {
+        $type = basename($type);
+        $class = 'Horde_Mobile_Renderer_' . $type;
+        if (!class_exists($class)) {
+            include_once 'Horde/Mobile/Renderer/' . $type . '.php';
+        }
+
+        if (class_exists($class)) {
+            $renderer = new $class($browser, $params);
+        } else {
+            $renderer = PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+
+        return $renderer;
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Mobile_Renderer instance
+     * based on $type. It will only create a new instance if no
+     * renderer with the same parameters currently exists.
+     *
+     * @param string $type      The kind of markup (html, hdml, wml) we want to
+     *                          generate.
+     * @param Browser $browser  The Browser object to use.
+     * @param array $params     A hash containing any options for the renderer.
+     *
+     * @return Horde_Mobile_Renderer  The newly created concrete
+     *                                Horde_Mobile_Renderer instance, or a
+     *                                PEAR_Error object on an error.
+     */
+    function &singleton($type, $browser = null, $params = array())
+    {
+        static $instances = array();
+
+        $signature = md5(serialize(array($type, $browser, $params)));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &Horde_Mobile_Renderer::factory($type, $browser, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Mobile/Mobile/Renderer/html.php b/framework/Mobile/Mobile/Renderer/html.php
new file mode 100644 (file)
index 0000000..9c436a5
--- /dev/null
@@ -0,0 +1,376 @@
+<?php
+/**
+ * Horde_Mobile_Renderer:: output module for simple HTML and
+ * Imode/Avantgo/similar devices.
+ *
+ * $Horde: framework/Mobile/Mobile/Renderer/html.php,v 1.46 2009/10/09 22:07:41 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_Renderer_html extends Horde_Mobile_Renderer {
+
+    /**
+     * Properly encode characters for output to an HTML browser.
+     *
+     * @param string $input  Characters to encode.
+     *
+     * @return string  The encoded text.
+     */
+    function escape($input)
+    {
+        return Horde_Text_Filter::filter($input, 'space2html', array('charset' => Horde_Nls::getCharset(), 'encode' => true));
+    }
+
+    /**
+     * Creates the page in the appropriate markup. Depending on the
+     * clients browser type pure HTML, handheldfriendly AvantGo HTML,
+     * i-mode cHTML, or MML is created.
+     *
+     * @param Horde_Mobile $deck  The deck to render.
+     */
+    function render($deck)
+    {
+        if ($deck->_debug) {
+            header('Content-Type: text/plain; charset=' . Horde_Nls::getCharset());
+        } else {
+            header('Content-Type: text/html; charset=' . Horde_Nls::getCharset());
+        }
+        header('Vary: Accept-Language');
+
+        if (!$this->isBrowser('mml')) {
+            echo "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
+        }
+
+        echo !empty($GLOBALS['language']) ? '<html lang="' . strtr($GLOBALS['language'], '_', '-') . '">' : '<html>';
+        echo '<head>';
+
+        if ($this->isBrowser('avantgo')) {
+            echo '<meta name="HandheldFriendly" content="True">';
+        }
+
+        printf("<title>%s</title>\n", $this->escape($deck->get('title')));
+
+        if ($deck->_simulator) {
+            // Use simulator (mobile theme) stylesheet.
+            echo Horde::stylesheetLink('horde', 'mobile');
+        }
+
+        echo '</head><body>';
+
+        if ($deck->_simulator) {
+            echo "<center><br />\n";
+            // Create default device simulator table layout with
+            // central CSS layout.
+            echo "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n";
+            echo "<tr><td colspan=\"3\" class=\"top\">&nbsp;</td></tr>\n";
+            echo "<tr><td valign=\"top\" class=\"left\">&nbsp;</td>\n";
+            echo "<td valign=\"top\" class=\"display\">\n";
+        }
+
+        $divstyle = '';
+        if ($this->hasQuirk('scroll_tds') && $deck->_simulator) {
+            // Make content of table element scrollable (Horde_Mobile
+            // simulator).
+            $divstyle = ' class="simdev"';
+        }
+        echo '<div' . $divstyle . '>';
+
+        if (($cnt = count($deck->_cards)) !== 0) {
+            $i = 0;
+            foreach ($deck->_cards as $card) {
+                if ($i != 0) {
+                    echo '<hr />';
+                }
+                $this->_renderCard($card);
+                $i++;
+            }
+        } else {
+            foreach ($deck->_elements as $page_element) {
+                $this->renderElement($page_element);
+            }
+        }
+
+        echo '</div>';
+
+        if ($deck->_simulator) {
+            // Display lower part of Horde_Mobile default device
+            // simulator.
+            echo '</td><td valign="top" class="right">&nbsp;</td></tr><tr><td colspan="3" class="bottom">&nbsp;</td></tr></table></center>';
+        }
+
+        echo '</body></html>';
+    }
+
+    function _renderCard($card)
+    {
+        $name = $card->get('name') ? ' name="' . $this->escape($card->get('name')) . '"' : '';
+        printf('<a%s>%s</a>', $name, $card->get('title'));
+
+        if (count($card->_softkeys)) {
+            foreach ($card->_softkeys as $key) {
+                echo ' | <a href="' . $key['url'] . '">' .  $this->escape($key['label']) . '</a>';
+            }
+        }
+
+        // Render all tags.
+        foreach ($card->_elements as $page_element) {
+            $this->renderElement($page_element);
+        }
+    }
+
+    function _renderLink($link)
+    {
+        if ($link->get('title') &&
+            !$this->isBrowser('avantgo') &&
+            !$this->isBrowser('imode') &&
+            !$this->isBrowser('mml')) {
+            $title_option = sprintf(' onmouseover="self.status=\'%s\';return true;"',
+                                    $this->escape($link->get('title')));
+        } else {
+            $title_option = '';
+        }
+
+        $accesskey_option = '';
+        if ($link->get('accesskey')) {
+            if ($this->isBrowser('imode')) {
+                $accesskey_option = sprintf(' accesskey="%d"', $link->get('accesskey'));
+            } elseif ($this->isBrowser('mml')) {
+                $accesskey_option = sprintf(' directkey="%d"', $link->get('accesskey'));
+            }
+        }
+
+        printf('<a href="%s"%s%s>%s</a>',
+               str_replace('&amp;amp;', '&amp;', $this->escape($link->get('url'))),
+               $title_option, $accesskey_option,
+               $this->escape($link->get('label')));
+    }
+
+    function _renderLinkset($linkset)
+    {
+        if (count($linkset->_elements)) {
+            echo '<ol>';
+            foreach ($linkset->_elements as $val) {
+                echo '<li>';
+                $this->_renderLink($val);
+                echo '</li>';
+            }
+            echo '</ol>';
+        }
+    }
+
+    function _renderText($element)
+    {
+        foreach ($element->_attributes as $attribute) {
+            echo '<' . $attribute . '>';
+        }
+
+        if ($element->get('linebreaks')) {
+            echo nl2br($this->escape($element->get('text')));
+        } else {
+            echo $this->escape($element->get('text'));
+        }
+
+        $attributes = array_reverse($element->_attributes);
+        foreach ($attributes as $attribute) {
+            echo '</' . $attribute . '>';
+        }
+    }
+
+    function _renderImage($image)
+    {
+        $attributes = '';
+        foreach ($image->_attributes as $attribute => $value) {
+            $attributes .= sprintf(' %s="%s"', $attribute, $value);
+        }
+        printf('<img src="%s.png"%s />', $image->_src, $attributes);
+    }
+
+    function _renderForm($form)
+    {
+        printf('<form action="%s" method="%s">', $form->get('url'), $form->get('method'));
+        parent::_renderForm($form);
+        echo '</form>';
+    }
+
+    function _renderInput($input)
+    {
+        $type = 'type="' . $input->get('type') . '"';
+        $size = $input->get('size') ? sprintf('size="%d"', $input->get('size')) : '';
+        $maxlength = $input->get('maxlength') ? sprintf('maxlength="%d"', $input->get('maxlength')) : '';
+
+        if ($this->isBrowser('imode')) {
+            $mode = sprintf(' istyle="%d"', $input->get('mode'));
+        } elseif ($this->isBrowser('mml')) {
+            $mode = $this->_getMode($input->get('mode'));
+        } else {
+            $mode = '';
+        }
+
+        // Create HTML input.
+        printf('%s <input %s name="%s" value="%s"%s%s%s/>',
+               $this->escape($input->get('label')), $type,
+               $this->escape($input->get('name')), $this->escape($input->get('value')), $size, $maxlength, $mode);
+    }
+
+    function _renderTextarea($textarea)
+    {
+        if ($this->isBrowser('imode')) {
+            $mode = sprintf(' istyle="%d"', $this->mode);
+        } elseif ($this->isBrowser('mml')) {
+            $mode = $this->_getMode($this->mode);
+        } else {
+            $mode = '';
+        }
+
+        printf('%s<br /><textarea name="%s" rows="%s" cols="%s"%s>%s</textarea>',
+               $this->escape($textarea->get('label')), $textarea->get('name'), $textarea->get('rows'),
+               $textarea->get('cols'), $mode, $textarea->get('value'));
+    }
+
+    function _renderSelect($select)
+    {
+        $name = $this->escape($select->get('name'));
+        echo '<label for="' . $name . '">';
+        if ($label = $select->get('label')) {
+            echo $this->escape($label) . ' ';
+        }
+        echo '<select id="' . $name . '" name="' . $name . '" size="1">';
+
+        $htmlchars = $select->get('htmlchars');
+        foreach ($select->_options as $val) {
+            if ($val['value'] == $select->_value) {
+                $sel = ' selected="selected"';
+            } else {
+                $sel = '';
+            }
+            $label = $htmlchars ? $val['label'] : $this->escape($val['label']);
+            echo '<option' . $sel . ' value="' . $this->escape($val['value']) . '">' . $label . '</option>';
+        }
+        echo '</select></label>';
+    }
+
+    function _renderRadio($radio)
+    {
+        foreach ($radio->_buttons as $val) {
+            $sel = ($val['value'] == $radio->_value) ? ' checked="checked"' : '';
+            printf('<input type="radio" name="%s"%s value="%s" /> %s<br />',
+                   $radio->get('name'), $sel, $val['value'],
+                   $this->escape($val['label']));
+        }
+    }
+
+    function _renderCheckbox($checkbox)
+    {
+        $state = $checkbox->isChecked() ? ' checked="checked"' : '';
+        printf('<label for="%1$s"><input type="checkbox" name="%1$s" id="%1$s"%2$s value="%3$s" /> %4$s</label><br />',
+               $checkbox->get('name'), $state, $checkbox->get('value'),
+               $this->escape($checkbox->get('label')));
+    }
+
+    function _renderSubmit($submit)
+    {
+        $name = !empty($submit->_name) ? ' name="' . $submit->_name . '"' : '';
+        printf('<input type="submit"%s value="%s" /><br />',
+               $name, $this->escape($submit->_label));
+    }
+
+    function _renderHidden($hidden)
+    {
+        printf('<input type="hidden" name="%s" value="%s" />',
+               $hidden->get('name'), $hidden->get('value'));
+    }
+
+    function _renderDl($dl)
+    {
+        echo '<dl>';
+
+        parent::_renderDl($dl);
+
+        // Terminate Dl.
+        if ($this->isBrowser('mml')) {
+            // MML has problems with the clear attribute.
+            echo '</dl><br />';
+        } else {
+            echo '</dl><br clear="all" />';
+        }
+    }
+
+    function _renderTable($table)
+    {
+        $border = $table->get('border');
+        $padding = $table->get('padding');
+        $spacing = $table->get('spacing');
+
+        echo '<table';
+        if (!is_null($border)) {
+            echo ' border="' . $border . '"';
+        }
+        if (!is_null($padding)) {
+            echo ' cellpadding="' . $padding . '"';
+        }
+        if (!is_null($spacing)) {
+            echo ' cellspacing="' . $spacing . '"';
+        }
+        echo '>';
+
+        parent::_renderTable($table);
+
+        // Terminate table.
+        if ($this->isBrowser('mml')) {
+            echo '</table><br />';
+        } else {
+            // MML has problems with the clear attribute.
+            echo '</table><br clear="all" />';
+        }
+    }
+
+    function _renderPhone($phone)
+    {
+        if ($this->isBrowser('imode')) {
+            // Create phoneto: link for i-Mode.
+            printf('<p><a href="phoneto:%s">%s</a></p>',
+                   $phone->get('number'), $phone->get('label'));
+        } elseif ($this->isBrowser('mml')) {
+            // Create tel: link for MML.
+            printf('<p><a href="tel:%s">%s</a></p>',
+                   $phone->get('number'), $phone->get('label'));
+        } else {
+            // Display phone number as plain text.
+            printf('<p><big>%s</big></p>', $phone->get('label'));
+        }
+    }
+
+    function _renderRule($rule)
+    {
+        $width = $rule->get('width');
+        $size = $rule->get('size');
+
+        echo '<hr' . ($width ? ' width="' . $width . '"' : '') . ($size ? ' size="' . $size . '"' : '') . " />\n";
+    }
+
+    function _getMode($mode)
+    {
+        switch ($mode) {
+        case 'katakana':
+            return ' mode="katakana"';
+
+        case 'hiragana':
+            return ' mode="hiragana"';
+
+        case 'numeric':
+            return ' mode="numeric"';
+
+        case 'alpha':
+        default:
+            return ' mode="alphabet"';
+        }
+    }
+
+}
diff --git a/framework/Mobile/Mobile/Renderer/wml.php b/framework/Mobile/Mobile/Renderer/wml.php
new file mode 100644 (file)
index 0000000..a612228
--- /dev/null
@@ -0,0 +1,343 @@
+<?php
+/**
+ * Horde_Mobile_Renderer:: output module for WML (Wireless Markup Language).
+ *
+ * $Horde: framework/Mobile/Mobile/Renderer/wml.php,v 1.49 2009/07/09 08:17:58 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Mobile
+ */
+class Horde_Mobile_Renderer_wml extends Horde_Mobile_Renderer {
+
+    /**
+     * Properly encode characters for output to a WML device.
+     *
+     * @param string $input  Characters to encode.
+     *
+     * @return string  The encoded text.
+     */
+    function escape($input)
+    {
+        // Encode entities.
+        $output = @htmlspecialchars($input, ENT_COMPAT, Horde_Nls::getCharset());
+
+        // Escape $ character in WML.
+        $output = str_replace('$', '$$', $output);
+
+        // Generate UTF-8.
+        $output = Horde_String::convertCharset($output, Horde_Nls::getCharset(), 'utf-8');
+
+        return $output;
+    }
+
+    /**
+     * Creates the page in WML, allowing for different WML browser quirks.
+     *
+     * @param Horde_Mobile $deck  The deck to render.
+     */
+    function render(&$deck)
+    {
+        if ($deck->_debug) {
+            header('Content-Type: text/plain; charset=utf-8');
+        } else {
+            header('Content-Type: text/vnd.wap.wml; charset=utf-8');
+        }
+
+        echo "<?xml version=\"1.0\"?>\n";
+        if ($this->hasQuirk('ow_gui_1.3')) {
+            echo '<!DOCTYPE wml PUBLIC "-//PHONE.COM//DTD WML 1.3//EN" "http://www.openwave.com/dtd/wml13.dtd">';
+        } else {
+            echo '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">';
+        }
+        echo '<wml>';
+
+        if (count($deck->_cards)) {
+            foreach ($deck->_cards as $card) {
+                $this->_renderCard($card);
+            }
+        } else {
+            $title = $deck->get('title') ? ' title="' . $this->escape($deck->get('title')) . '"' : '';
+            printf('<card%s>', $title);
+
+            // Render all tags.
+            foreach ($deck->_elements as $page_element) {
+                $this->renderElement($page_element);
+            }
+
+            echo '</card>';
+        }
+
+        // End the WML page.
+        echo '</wml>';
+    }
+
+    function _renderCard(&$card)
+    {
+        $name = $card->get('name') ? ' id="' . $this->escape($card->get('name')) . '"' : '';
+        $title = $card->get('title') ? ' title="' . $this->escape($card->get('title')) . '"' : '';
+        printf('<card%s%s>', $name, $title);
+
+        // Initialize WML variables with their default values.
+        if (!is_null($card->_form)) {
+            echo '<onevent type="onenterforward"><refresh>';
+            $defaults = $card->_form->getDefaults();
+            foreach ($defaults as $d) {
+                printf('<setvar name="_%s" value="%s"/>', $d['name'], $this->escape($d['value']));
+            }
+            echo '</refresh></onevent>';
+        }
+
+        if (count($card->_softkeys)) {
+            if (count($card->_softkeys) == 1) {
+                // If there is only one softkey, make it of type
+                // 'options' so that it always shows up on the right,
+                // instead of having to share the left softkey with
+                // active links, making it much harder to get to.
+                $type = 'options';
+            } else {
+                $type = 'accept';
+            }
+            foreach ($card->_softkeys as $key) {
+                echo '<do type="' . $type . '" label="' . $this->escape($key['label']) . '"><go href="' . $key['url'] . '"/></do>';
+            }
+        }
+
+        // Render all tags.
+        foreach ($card->_elements as $page_element) {
+            $this->renderElement($page_element);
+        }
+
+        echo '</card>';
+    }
+
+    function _renderLink(&$link)
+    {
+        $title_option = $link->get('title') ? sprintf(' title="%s"', $this->escape($link->get('title'))) : '';
+
+        printf('<a%s href="%s">%s</a>',
+               $title_option, str_replace('&amp;amp;', '&amp;', $this->escape($link->get('url'))),
+               $this->escape($link->get('label')));
+    }
+
+    function _renderLinkset(&$linkset)
+    {
+        if (count($linkset->_elements)) {
+            echo '<p>';
+            if ($this->isBrowser('up')) {
+                echo '<select>';
+                foreach ($linkset->_elements as $val) {
+                    $title = $val->get('title') ? ' title="' . $this->escape($val->get('title')) . '"' : '';
+                    printf('<option onpick="%s"%s>%s</option>',
+                           str_replace('&amp;amp;', '&amp;', $this->escape($val->get('url'))),
+                           $title,
+                           $this->escape($val->get('label')));
+                }
+                echo '</select>';
+            } else {
+                foreach ($linkset->_elements as $val) {
+                    $this->_renderLink($val);
+                    echo '<br />';
+                }
+            }
+            echo '</p>';
+        }
+    }
+
+    function _renderText(&$element)
+    {
+        foreach ($element->_attributes as $attribute) {
+            echo '<' . $attribute . '>';
+        }
+
+        if ($element->get('linebreaks')) {
+            echo nl2br($this->escape($element->get('text')));
+        } else {
+            echo $this->escape($element->get('text'));
+        }
+
+        $attributes = array_reverse($element->_attributes);
+        foreach ($attributes as $attribute) {
+            echo '</' . $attribute . '>';
+        }
+    }
+
+    function _renderImage(&$image)
+    {
+        $attributes = '';
+        foreach ($image->_attributes as $attribute => $value) {
+            $attributes .= sprintf(' %s="%s"', $attribute, $value);
+        }
+        printf('<img src="%s.wbmp"%s/>', $image->_src, $attributes);
+    }
+
+    function _renderInput(&$input)
+    {
+        $type = ' type="' . $input->get('type') . '"';
+        $size = $input->get('size') ? sprintf(' size="%d"', $input->get('size')) : '';
+        $maxlength = $input->get('maxlength') ? sprintf(' maxlength="%d"', $input->get('maxlength')) : '';
+
+        printf('%s<input emptyok="true" format="%s"%s name="_%s" value="%s"%s%s/>',
+               $this->escape($input->get('label')), $input->get('format'),
+               $type, $this->escape($input->get('name')), $this->escape($input->get('value')), $size, $maxlength);
+    }
+
+    function _renderTextarea(&$textarea)
+    {
+        printf('%s<input emptyok="true" name="_%s" value="%s"/>',
+               $this->escape($textarea->get('label')),
+               $textarea->get('name'), $textarea->get('value'));
+    }
+
+    function _renderSelect(&$select)
+    {
+        if ($label = $select->get('label')) {
+            echo $this->escape($label) . ' ';
+        }
+
+        if ($this->hasQuirk('ow_gui_1.3')) {
+            switch ($select->get('type')) {
+            case 'spin':
+                $type_option = 'type="spin"';
+                break;
+
+            case 'popup':
+            default:
+                $type_option = 'type="popup"';
+                break;
+            }
+
+            echo '<select ' . $type_option . ' name="_' . $select->get('name') . '">';
+        } else {
+            echo '<select name="_' . $select->get('name') . '">';
+        }
+
+        $htmlchars = $select->get('htmlchars');
+        foreach ($select->_options as $val) {
+            $label = $htmlchars ? $val['label'] : $this->escape($val['label']);
+            echo '<option value="' . $val['value'] . '">' . $label . '</option>';
+        }
+        echo '</select>';
+    }
+
+    function _renderRadio(&$radio)
+    {
+        if ($this->hasQuirk('ow_gui_1.3')) {
+            // Openwave GUI extensions for WML 1.3
+            printf('<select type="radio" name="_%s">', $radio->get('name'));
+        } else {
+            // Conventional WML (similar to Horde_Mobile_select).
+            printf('<select name="_%s">', $radio->get('name'));
+        }
+
+        foreach ($radio->_buttons as $val) {
+            printf('<option value="%s">%s</option>',
+                   $val['value'], $this->escape($val['label']));
+        }
+
+        echo '</select>';
+    }
+
+    function _renderCheckbox(&$checkbox)
+    {
+        printf('<select name="_%s" multiple="true">', $checkbox->get('name'));
+        printf('<option value="%s">%s</option></select>',
+               $checkbox->get('value'), $this->escape($checkbox->get('label')));
+    }
+
+    function _renderSubmit(&$submit)
+    {
+        if ($this->hasQuirk('ow_gui_1.3')) {
+            // Create <do type="button"> sequence for Openwave GUI
+            // extensions WML 1.3.
+            printf('<do type="button" label="%s">',
+                   $this->escape($submit->get('label')));
+            $tag = 'do';
+        } else {
+            // Create <anchor> sequence in normal WML.
+            printf('<anchor title="%s">%s',
+                   $this->escape($submit->get('label')),
+                   $this->escape($submit->get('label')));
+            $tag = 'anchor';
+        }
+
+        if ($submit->_form->get('method') == 'post') {
+            printf('<go href="%s" method="post">', Horde::url($submit->_form->get('url')));
+
+            // Value for this submit element, only if non-empty name.
+            if ($submit->get('name')) {
+                printf('<postfield name="%s" value="%s"/>', $submit->get('name'), $this->escape($submit->get('label')));
+            }
+
+            $defaults = $submit->_form->getDefaults();
+            foreach ($defaults as $d) {
+                if (array_key_exists('hidden', $d)) {
+                    printf('<postfield name="%s" value="%s"/>', $d['name'], $this->escape($d['value']));
+                } else {
+                    printf('<postfield name="%s" value="$(_%s)"/>', $d['name'], $d['name']);
+                }
+            }
+        } else {
+            // Start with the value for this submit element.
+            $query_string = $submit->get('name') . '=' . $this->escape($submit->get('label')) . '&amp;';
+
+            $getvars = $submit->_form->getGetVars();
+            foreach ($getvars as $val) {
+                $query_string .= $val . '=$(_' . $val . ')&amp;';
+            }
+
+            if (substr($query_string, -5) == '&amp;') {
+                $query_string = substr($query_string, 0, strlen($query_string) - 5);
+            }
+
+            printf('<go href="%s?%s">', $submit->_form->get('url'), $query_string);
+        }
+
+        echo "</go></$tag>";
+    }
+
+    function _renderTable(&$table)
+    {
+        // Count maximum number of columns in table.
+        $max = 0;
+        foreach ($table->_rows as $row) {
+            $max = max($max, $row->getColumnCount());
+        }
+        printf('<p><table columns="%d">', $max);
+
+        parent::_renderTable($table);
+
+        // Terminate table.
+        echo '</table></p>';
+    }
+
+    function _renderPhone(&$phone)
+    {
+        $title = $phone->get('title');
+        $title_option = ($title ? sprintf(' title="%s"', $this->escape($title)) : '');
+
+        printf('<a%s href="wtai://wp/mc;%s">%s</a>', $title_option,
+               str_replace('+', '%2B', $phone->get('number')), $phone->get('label'));
+    }
+
+    function _renderRule(&$rule)
+    {
+        if ($this->hasQuirk('ow_gui_1.3')) {
+            // WAP device accepts Openwave GUI extensions for WML 1.3
+            $width = $rule->get('width');
+            $size = $rule->get('size');
+
+            echo '<hr' . ($width ? ' width="' . $width . '"' : '') . ($size ? ' size="' . $size . '"' : '') . ' />';
+        } else {
+            // WAP device does not understand <hr /> tags.
+            // ==> draw some number of hyphens to create a rule
+            echo '----------<br />';
+        }
+    }
+
+}
diff --git a/framework/Mobile/package.xml b/framework/Mobile/package.xml
new file mode 100644 (file)
index 0000000..399db2c
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Mobile</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Mobile API</summary>
+ <description>Horde API for generating Mobile content. Includes numerous utility functions, generalized element classes, and renderers for markup languages including WML, HDML, and CHTML.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>22:33:35</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="Mobile">
+    <dir name="Renderer">
+     <file baseinstalldir="/Horde" name="html.php" role="php" />
+     <file baseinstalldir="/Horde" name="wml.php" role="php" />
+    </dir> <!-- //Mobile/Renderer -->
+    <file baseinstalldir="/Horde" name="Renderer.php" role="php" />
+   </dir> <!-- //Mobile -->
+   <file baseinstalldir="/Horde" name="Mobile.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Net_IMSP/IMSP.php b/framework/Net_IMSP/IMSP.php
new file mode 100644 (file)
index 0000000..75b49f8
--- /dev/null
@@ -0,0 +1,475 @@
+<?php
+
+include_once 'Log.php';
+
+// Constant Definitions
+define('IMSP_OCTET_COUNT', "/({)([0-9]{1,})(\}$)/");
+define('IMSP_MUST_USE_LITERAL', "/[\x80-\xFF\\r\\n\"\\\\]/");
+define('IMSP_MUST_USE_QUOTE', "/[\W]/i");
+
+/**
+ * The Net_IMSP class provides a common interface to an IMSP server .
+ *
+ * Required parameters:<pre>
+ *   'server'  Hostname of IMSP server.
+ *   'port'    Port of IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP.php,v 1.45 2009/10/02 00:01:02 mrubinsk Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP {
+
+    /**
+     * String containing name/IP address of IMSP host.
+     *
+     * @var string
+     */
+    var $imsp_server                = 'localhost';
+
+    /**
+     * String containing port for IMSP server.
+     *
+     * @var string
+     */
+    var $imsp_port                  = '406';
+
+    /**
+     * Boolean to set if we should write to a log, if one is set up.
+     *
+     * @var boolean
+     */
+    var $logEnabled                 = true;
+
+    /**
+     * String buffer containing the last raw NO or BAD response from the
+     * server.
+     *
+     * @var string
+     */
+    var $lastRawError              = '';
+
+    // Private Declarations
+    var $_commandPrefix             = 'A';
+    var $_commandCount              = 1;
+    var $_tag                       = '';
+    var $_stream                    = null;
+    var $_lastCommandTag            = 'undefined';
+    var $_logger                    = null;
+    var $_logSet                    = null;
+    var $_logLevel                  = PEAR_LOG_INFO;
+    var $_logBuffer                  = array();
+
+    /**
+     * Constructor function.
+     *
+     * @param array $params Hash containing server parameters.
+     */
+    function Net_IMSP($params)
+    {
+        if (is_array($params) && !empty($params['server'])) {
+            $this->imsp_server = $params['server'];
+        }
+
+        if (is_array($params) && !empty($params['port'])) {
+            $this->imsp_port = $params['port'];
+        }
+
+    }
+
+    /**
+     * Initialization function to be called after object is returned.  This
+     * allows errors to occur and not break the script.
+     *
+     * @return mixed  True on success PEAR_Error on connection failure.
+     */
+    function init()
+    {
+        $result = $this->imspOpen();
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $this->writeToLog('Initializing Net_IMSP object.', __FILE__, __LINE__,
+                          PEAR_LOG_INFO);
+        return true;
+    }
+
+    /**
+     * Logs out of the server and closes the IMSP stream
+     */
+    function logout()
+    {
+        $this->writeToLog('Closing IMSP Connection.', __FILE__, __LINE__,
+                          PEAR_LOG_INFO);
+        $command_string = 'LOGOUT';
+        $result = $this->imspSend($command_string);
+        if (is_a($result, 'PEAR_Error')) {
+            fclose($this->_stream);
+            return $result;
+        } else {
+            fclose($this->_stream);
+            return true;
+        }
+    }
+
+    /**
+     * Returns the raw capability response from the server.
+     *
+     * @return string  The raw capability response.
+     */
+    function capability()
+    {
+        $command_string = 'CAPABILITY';
+        $result = $this->imspSend($command_string);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } else {
+            $server_response = $this->imspReceive();
+            if (preg_match("/^\* CAPABILITY/", $server_response)) {
+                $capability = preg_replace("/^\* CAPABILITY/",
+                                           '', $server_response);
+
+                $server_response = $this->imspReceive(); //OK
+
+                if (!$server_response == 'OK') {
+                    return $this->imspError('Did not receive the expected response from the server.',
+                                            __FILE__, __LINE__);
+                } else {
+                    $this->writeToLog('CAPABILITY completed OK', __FILE__,
+                                      __LINE__, PEAR_LOG_INFO);
+                    return $capability;
+                }
+            }
+        }
+    }
+
+    /**
+     * Attempts to open an IMSP socket with the server.
+     *
+     * @return mixed  True on success PEAR_Error on failure.
+     */
+    function imspOpen()
+    {
+        $fp = @fsockopen($this->imsp_server, $this->imsp_port);
+        if (!$fp) {
+            return $this->imspError('Connection to IMSP host failed.', __FILE__,
+                                    __LINE__);
+        }
+        $this->_stream = $fp;
+        $server_response = $this->imspReceive();
+        if (!preg_match("/^\* OK/", $server_response)) {
+            fclose($fp);
+            return $this->imspError('Did not receive the expected response from the server.', __FILE__, __LINE__);
+        }
+        return true;
+    }
+
+    /**
+     * Attempts to send a command to the server.
+     *
+     * @param string  $commandText Text to send to the server.
+     * @param boolean $includeTag  Determines if command tag is prepended.
+     * @param boolean  $sendCRLF   Determines if CRLF is appended.
+     * @return mixed   True on success PEAR_Error on failure.
+     */
+    function imspSend($commandText, $includeTag=true, $sendCRLF=true)
+    {
+        $command_text = '';
+
+        if (!$this->_stream) {
+            return $this->imspError('Connection to IMSP host failed.', __FILE__, __LINE__);
+        }
+
+        if ($includeTag) {
+            $this->_tag = $this->_getNextCommandTag();
+            $command_text = "$this->_tag ";
+        }
+
+        $command_text .= $commandText;
+
+        if ($sendCRLF) {
+            $command_text .= "\r\n";
+        }
+
+        $this->writeToLog('To: ' . $command_text, __FILE__,
+                          __LINE__, PEAR_LOG_DEBUG);
+
+        if (!fputs($this->_stream, $command_text)) {
+            return $this->imspError('Connection to IMSP host failed.', __FILE__, __LINE__);
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Receives a single CRLF terminated server response string
+     *
+     * @return mixed 'NO', 'BAD', 'OK', raw response or PEAR_Error.
+     */
+    function imspReceive()
+    {
+        if (!$this->_stream) {
+            return $this->imspError('Connection to IMSP host failed.', __FILE__, __LINE__);
+        }
+        $result = fgets($this->_stream, 512);
+        if (!$result) {
+            return $this->imspError('Did not receive the expected response from the server.',
+                                    __FILE__, __LINE__);
+        }
+        $meta = stream_get_meta_data($this->_stream);
+        if ($meta['timed_out']) {
+            return $this->imspError('Connection to IMSP host failed.' . ': Connection timed out!',
+                                    __FILE__, __LINE__);
+        }
+
+        $server_response = trim($result);
+        $this->writeToLog('From: ' . $server_response, __FILE__,
+                          __LINE__, PEAR_LOG_DEBUG);
+
+        /* Parse out the response:
+         * First make sure that this is not for a previous command.
+         * If it is, it means we did not read all the server responses from
+         * the last command...read them now, but throw an error. */
+        while (preg_match("/^" . $this->_lastCommandTag
+                          ."/", $server_response)) {
+            $server_response =
+                trim(fgets($this->_stream, 512));
+            $this->imspError('Did not receive the expected response from the server.' . ": $server_response",
+                             __FILE__, __LINE__);
+        }
+
+        $currentTag = $this->_tag;
+        if (preg_match("/^" . $currentTag . " NO/", $server_response)) {
+            $this->lastRawError = $server_response;
+            return 'NO';
+        }
+
+        if (preg_match("/^" . $currentTag . " BAD/", $server_response)) {
+            $this->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+            $this->lastRawError = $server_response;
+            return 'BAD';
+        }
+
+        if (preg_match("/^" . $currentTag . " OK/", $server_response)) {
+            return 'OK';
+        }
+
+        /* If it was not a 'NO', 'BAD' or 'OK' response,
+         * then it's up to the calling function to decide
+         * what to do with it. */
+        return $server_response;
+    }
+
+    /**
+     * Retrieves CRLF terminated response from server and splits it into
+     * an array delimited by a <space>.
+     *
+     * @return array result from explode().
+     */
+    function getServerResponseChunks()
+    {
+        $server_response =
+            trim(fgets($this->_stream, 512));
+        $chunks = explode(' ', $server_response);
+        return $chunks;
+    }
+
+    /*
+     * Receives fixed number of bytes from IMSP socket. Used when
+     * server returns a string literal.
+     *
+     * @param integer $length  Number of bytes to read from socket.
+     *
+     * @return string  Text of string literal.
+     */
+    function receiveStringLiteral($length)
+    {
+        $literal = '';
+        do {
+            $temp = fread($this->_stream, $length);
+            $length -= strlen($temp);
+            $literal .= $temp;
+        } while ($length > 0 && strlen($temp));
+        $this->writeToLog('From{}: ' . $literal, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $literal;
+    }
+
+    /**
+     * Increments the IMSP command tag token.
+     *
+     * @access private
+     * @return string Next command tag.
+     */
+    function _getNextCommandTag()
+    {
+        $this->_lastCommandTag = $this->_tag ? $this->_tag : 'undefined';
+        return $this->_commandPrefix . sprintf('%04d', $this->_commandCount++);
+    }
+
+    /**
+     * Determines if a string needs to be quoted before sending to the server.
+     *
+     * @param string $string  String to be tested.
+     * @return string Original string quoted if needed.
+     */
+    function quoteSpacedString($string)
+    {
+        if (strpos($string, ' ') !== false ||
+            preg_match(IMSP_MUST_USE_QUOTE, $string)) {
+            return '"' . $string . '"';
+        } else {
+            return $string;
+        }
+    }
+
+    /**
+     * Raises an IMSP error.  Basically, only writes
+     * error out to the horde logfile and returns PEAR_Error
+     *
+     * @param string $err    Either PEAR_Error object or text to write to log.
+     * @param string $file   File name where error occured.
+     * @param integer $line  Line number where error occured.
+     */
+    function imspError($err = '', $file=__FILE__, $line=__LINE__)
+    {
+        if (is_a($err, 'PEAR_Error')) {
+            $log_text = $err->getMessage();
+        } else {
+            $log_text = $err;
+        }
+
+        $this->writeToLog($log_text, $file, $line, PEAR_LOG_ERR);
+        if (is_a($err, 'PEAR_Error')) {
+            return $err;
+        } else {
+            return PEAR::raiseError($err);
+        }
+    }
+
+    /**
+     * Writes a message to the IMSP logfile.
+     *
+     * @param string $message  Text to write.
+     */
+    function writeToLog($message, $file = __FILE__,
+                        $line = __LINE__, $priority = PEAR_LOG_INFO)
+    {
+        if (($this->logEnabled) && ($this->_logSet)) {
+            if ($priority > $this->_logLevel) {
+                return;
+            }
+
+            $logMessage = '[imsp] ' . $message . ' [on line ' . $line . ' of "' . $file . '"]';
+            $this->_logger->log($logMessage, $priority);
+        } elseif ((!$this->_logSet) && ($this->logEnabled)) {
+            $this->_logBuffer[] = array('message'  => $message,
+                                        'priority' => $priority,
+                                        'file'     => $file,
+                                        'line'     => $line
+                                        );
+        }
+    }
+
+    /**
+     * Creates a new Log object based on $params
+     *
+     * @param  array  $params Log object parameters.
+     * @return mixed  True on success or PEAR_Error on failure.
+     */
+    function setLogger($params)
+    {
+        if (!empty($params['enabled'])) {
+            $this->_logLevel = $params['priority'];
+            $logger = &Log::singleton($params['type'], $params['name'],
+                                      $params['ident'], $params['params']);
+
+            if (is_a($logger, 'PEAR_Error')) {
+                $this->logEnabled = false;
+                $this->_logSet = false;
+                return $logger;
+            } else {
+                $this->_logSet = true;
+                $this->_logger = &$logger;
+                $this->logEnabled = true;
+                $this->_writeLogBuffer();
+                return true;
+            }
+        } else {
+            $this->logEnabled = false;
+        }
+    }
+
+    /**
+     * Writes out contents of $_logBuffer to log file.  Allows messages
+     * to be logged during initialization of object before Log object is
+     * instantiated.
+     *
+     * @access private
+     */
+    function _writeLogBuffer()
+    {
+        for ($i = 0; $i < count($this->_logBuffer); $i++) {
+            $this->writeToLog($this->_logBuffer[$i]['message'],
+                              $this->_logBuffer[$i]['file'],
+                              $this->_logBuffer[$i]['line'],
+                              $this->_logBuffer[$i]['priority']);
+        }
+    }
+
+    /**
+     * Attempts to create a Net_IMSP object based on $driver.
+     * Must be called as $imsp = &Net_IMSP::factory($driver, $params);
+     *
+     * @param  string $driver Type of Net_IMSP object to return.
+     * @param  mixed  $params  Any parameters needed by the Net_IMSP object.
+     *
+     * @return mixed  The requested Net_IMSP object.
+     * @throws Horde_Exception
+     */
+    function factory($driver, $params)
+    {
+        $driver = basename($driver);
+        if (empty($driver) || $driver == 'none') {
+            return new Net_IMSP($params);
+        }
+
+        include_once dirname(__FILE__) . '/IMSP/' . $driver . '.php';
+        $class = 'Net_IMSP_' . $driver;
+        if (class_exists($class)) {
+            return new $class($params);
+        }
+
+         throw new Horde_Exception(sprintf(_("Unable to load the definition of %s."), $class));
+    }
+
+    /**
+     * Attempts to return a Net_IMSP object based on $driver.  Only
+     * creates a new object if one with the same parameters already
+     * doesn't exist.
+     * Must be called as $imsp = &Net_IMSP::singleton($driver, $params);
+     *
+     * @param  string $driver Type of Net_IMSP object to return.
+     * @param  mixed  $params Any parameters needed by the Net_IMSP object.
+     * @return mixed  Reference to the Net_IMSP object or PEAR_Error on failure.
+     */
+    function &singleton($driver, $params)
+    {
+        static $instances = array();
+
+        $signature = serialize(array($driver, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = Net_IMSP::factory($driver, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Auth.php b/framework/Net_IMSP/IMSP/Auth.php
new file mode 100644 (file)
index 0000000..2a56b03
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+
+require_once 'Net/IMSP.php';
+
+/**
+ * The Net_IMSP_Auth class abstract class for IMSP authentication.
+ *
+ * Required Parameters:<pre>
+ *   'username'  Username to logon to IMSP server as.
+ *   'password'  Password for current user.
+ *   'server'    The hostname of the IMSP server.
+ *   'port'      The port of the IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Auth.php,v 1.26 2009/07/17 21:00:12 slusarz Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Auth {
+    /**
+     * Class variable to hold the resulting Net_IMSP object
+     *
+     * @var Net_IMSP
+     */
+    var $_imsp;
+
+     /**
+     * Attempts to login to IMSP server.
+     *
+     * @param array $params         Parameters for Net_IMSP
+     * @param boolean $login        Should we remain logged in after auth?
+     *
+     * @return mixed                Returns a Net_IMSP object connected to
+     *                              the IMSP server if login is true and
+     *                              successful.  Returns boolean true if
+     *                              successful and login is false. Returns
+     *                              PEAR_Error on failure.
+     */
+    function &authenticate($params, $login = true)
+    {
+        $this->_imsp = &$this->_authenticate($params);
+
+        if (is_a($this->_imsp, 'PEAR_Error')) {
+            return $this->_imsp;
+        }
+
+        if (!$login) {
+            $this->_imsp->logout();
+            return true;
+        }
+
+        return $this->_imsp;
+    }
+
+    /**
+     * Private authentication function. Provides actual authentication
+     * code.
+     *
+     * @access private
+     * @param  array   $params      Parameters for Net_IMSP_Auth driver.
+     *
+     * @return mixed                Returns Net_IMSP object connected to server
+     *                              if successful, PEAR_Error on failure.
+     * @abstract
+     */
+    function _authenticate($params)
+    {
+
+    }
+
+    /**
+     * Returns the type of this driver.
+     *
+     * @abstract
+     * @return string Type of IMSP_Auth driver instance
+     */
+    function getDriverType()
+    {
+
+    }
+
+    /**
+     * Force a logout from the underlying IMSP stream.
+     *
+     */
+    function logout()
+    {
+
+    }
+
+    /**
+     * Attempts to return a concrete Net_IMSP_Auth instance based on $driver
+     * Must be called as &Net_IMSP_Auth::factory()
+     *
+     * @param  string $driver Type of Net_IMSP_Auth subclass to return.
+     *
+     * @return mixed  The created Net_IMSP_Auth subclass.
+     * @throws Horde_Exception
+     */
+    function factory($driver)
+    {
+        $driver = basename($driver);
+
+        if (empty($driver) || (strcmp($driver, 'none') == 0)) {
+            return new Net_IMSP_Auth();
+        }
+
+        if (file_exists(dirname(__FILE__) . '/Auth/' . $driver . '.php')) {
+            require_once dirname(__FILE__) . '/Auth/' . $driver . '.php';
+        }
+
+        $class = 'Net_IMSP_Auth_' . $driver;
+
+        if (class_exists($class)) {
+            return new $class();
+        }
+
+        throw new Horde_Exception(sprintf(_("Unable to load the definition of %s."), $class));
+    }
+
+    /**
+     * Attempts to return a concrete Net_IMSP_Auth instance based on $driver.
+     * Will only create a new object if one with the same parameters already
+     * does not exist.
+     * Must be called like: $var = &Net_IMSP_Auth::singleton('driver_type');
+     *
+     * @param  string $driver Type of IMSP_Auth subclass to return.
+     *
+     * @return object Reference to IMSP_Auth subclass.
+     */
+    function &singleton($driver)
+    {
+        static $instances;
+        /* Check for any imtest driver instances and kill them.
+           Otherwise, the socket will hang between requests from
+           seperate drivers (an Auth request and an Options request).*/
+        if (is_array($instances)) {
+            foreach ($instances as $obj) {
+                if ($obj->getDriverType() == 'imtest') {
+                    $obj->logout();
+                }
+            }
+        }
+        if (!isset($instances)) {
+            $instances = array();
+        }
+
+        $signature = serialize(array($driver));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = Net_IMSP_Auth::factory($driver);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Auth/cram_md5.php b/framework/Net_IMSP/IMSP/Auth/cram_md5.php
new file mode 100644 (file)
index 0000000..a19202f
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+/**
+ * The Net_IMSP_Auth_cram_md5 class for IMSP authentication.
+ *
+ * Required parameters:<pre>
+ *   'username'  Username to logon to IMSP server as.
+ *   'password'  Password for current user.
+ *   'server'    The hostname of the IMSP server.
+ *   'port'      The port of the IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Auth/cram_md5.php,v 1.21 2009/01/06 17:49:34 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Auth_cram_md5 extends Net_IMSP_Auth {
+
+    /**
+     * Private authentication function.  Provides actual
+     * authentication code.
+     *
+     * @access private
+     * @param  mixed $params Hash of IMSP parameters.
+     *
+     * @return mixed Net_IMSP object connected to server if successful,
+     *               PEAR_Error on failure.
+     */
+    function &_authenticate($params)
+    {
+        $imsp = &Net_IMSP::singleton('none', $params);
+        $result = $imsp->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $userId = $params['username'];
+        $credentials = $params['password'];
+        $result = $imsp->imspSend('AUTHENTICATE CRAM-MD5');
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Get response and decode it. Note that we remove the 1st 2
+         * characters from the response to get rid of the '+'
+         * continuation character and the space that is sent as part
+         * of the CRAM-MD5 response (at least on cyrus-imspd). */
+        $server_response = $imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+
+        $server_response = base64_decode(trim(substr($server_response, 2)));
+
+        /* Build and base64 encode the response to the challange. */
+        $response_to_send = $userId . ' ' . $this->_hmac($credentials, $server_response);
+        $command_string = base64_encode($response_to_send);
+
+        /* Send the response. */
+        $result = $imsp->imspSend($command_string, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $imsp->imspReceive();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if ($result != 'OK') {
+            $result = $imsp->imspError('Login to IMSP host failed.', __FILE__, __LINE__);
+            return $result;
+        } else {
+            return $imsp;
+        }
+    }
+
+    /**
+     * RFC 2104 HMAC implementation.
+     *
+     * @access private
+     * @param  string  $key    The HMAC key.
+     * @param  string  $data   The data to hash with the key.
+     *
+     * @return string  The MD5 HMAC.
+     */
+    function _hmac($key, $data)
+    {
+        if (function_exists(hash_hmac)) {
+            return hash_hmac('md5', $data, $key);
+        }
+
+        /* Byte length for md5. */
+        $b = 64;
+
+        if (strlen($key) > $b) {
+            $key = pack('H*', md5($key));
+        }
+
+        $key = str_pad($key, $b, chr(0x00));
+        $ipad = str_pad('', $b, chr(0x36));
+        $opad = str_pad('', $b, chr(0x5c));
+        $k_ipad = $key ^ $ipad;
+        $k_opad = $key ^ $opad;
+        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
+    }
+
+    /**
+     * Force a logout command to the imsp stream.
+     *
+     */
+    function logout()
+    {
+        $this->_imsp->logout();
+    }
+
+    /**
+     * Return the driver type
+     *
+     * @return string the type of this IMSP_Auth driver
+     */
+     function getDriverType()
+     {
+         return 'cram_md5';
+     }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Auth/imtest.php b/framework/Net_IMSP/IMSP/Auth/imtest.php
new file mode 100755 (executable)
index 0000000..9a5bce8
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * The Net_IMSP_Auth_imtest class for IMSP authentication.
+ *
+ * Required parameters:<pre>
+ *   'username'        Username to logon to IMSP server as.
+ *   'password'        Password for current user.
+ *   'server'          The hostname of the IMSP server.
+ *   'port'            The port of the IMSP server.
+ *   'socket'          The named socket to use for connection
+ *   'command'         Path to the imtest command on localhost
+ *   'auth_mechanism'  Authentication method to use with imtest</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Auth/imtest.php,v 1.19 2009/01/06 17:49:34 jan Exp $
+ *
+ * Copyright 2005-2007      Liam Hoekenga <liamr@umich.edu>
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Liam Hoekenga <liamr@umich.edu>
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Auth_imtest extends Net_IMSP_Auth {
+
+    /**
+     * Private authentication function.  Provides actual
+     * authentication code.
+     *
+     * @access private
+     * @param  mixed  $params Hash of IMSP parameters.
+     *
+     * @return mixed  Net_IMSP object connected to server if successful,
+     *                PEAR_Error on failure.
+     */
+    function &_authenticate($params)
+    {
+        $imsp = &Net_IMSP::singleton('none', $params);
+        $command = '';
+        $error_return = '';
+
+        if (strtolower($params['auth_mechanism']) == 'gssapi' &&
+            isset($_SERVER['KRB5CCNAME'])) {
+                $command .= 'KRB5CCNAME=' . $_SERVER['KRB5CCNAME'];
+        }
+
+        $command .= ' '    . $params['command'].
+                    ' -m ' . $params['auth_mechanism'] .
+                    ' -u ' . escapeshellarg($params['username']) .
+                    ' -a ' . escapeshellarg($params['username']) .
+                    ' -w ' . escapeshellarg($params['password']).
+                    ' -p ' . $params['port'] .
+                    ' -X ' . $params['socket'] .
+                    ' '    . $params['server'];
+
+        $conn_attempts = 0;
+        while ($conn_attempts++ < 4) {
+            $attempts = 0;
+            if (!file_exists($params['socket'])) {
+                exec($command . ' > /dev/null 2>&1');
+                sleep(1);
+                while (!file_exists($params['socket'])) {
+                    usleep(200000);
+                    if ($attempts++ > 5) {
+                        $error_return = ': No socket after 10 seconds of trying!';
+                        continue 2;
+                    }
+                }
+            }
+            $fp = @fsockopen($params['socket'], 0, $error_number, $error_string, 30);
+            $error_return = $error_string;
+            if ($fp) break;
+            unlink($params['socket']);
+
+        }
+        //Failure?
+        if (!empty($error_return)) {
+            return $imsp->imspError('Connection to IMSP host failed.' . ': ' . $error_return, __FILE__, __LINE__);
+        }
+        //Success
+        $imsp->_stream = $fp;
+        return $imsp;
+    }
+
+    /**
+     * Force a logout command to the imsp stream.
+     *
+     */
+    function logout()
+    {
+        $this->_imsp->logout();
+    }
+
+    /**
+     * Returns the driver type.
+     *
+     * @return string  The type of this IMSP_Auth driver.
+     */
+    function getDriverType()
+    {
+        return 'imtest';
+    }
+}
diff --git a/framework/Net_IMSP/IMSP/Auth/plaintext.php b/framework/Net_IMSP/IMSP/Auth/plaintext.php
new file mode 100644 (file)
index 0000000..a00ba73
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * The Net_IMSP_Auth_plaintext class for IMSP authentication.
+ *
+ * Required parameters:<pre>
+ *   'username'  Username to logon to IMSP server as.
+ *   'password'  Password for current user.
+ *   'server'    The hostname of the IMSP server.
+ *   'port'      The port of the IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Auth/plaintext.php,v 1.22 2009/01/06 17:49:34 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Auth_plaintext extends Net_IMSP_Auth {
+
+    /**
+     * Private authentication function.  Provides actual
+     * authentication code.
+     *
+     * @access private
+     * @param  mixed  $params Hash of IMSP parameters.
+     *
+     * @return mixed  Net_IMSP object connected to server if successful,
+     *                PEAR_Error on failure.
+     */
+    function &_authenticate($params)
+    {
+        $imsp = &Net_IMSP::singleton('none', $params);
+        $result = $imsp->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+         $userId = $params['username'];
+         $credentials = $params['password'];
+
+        /* Start the command. */
+        $result = $imsp->imspSend('LOGIN ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Username as a {}? */
+        if (preg_match(IMSP_MUST_USE_LITERAL, $userId)) {
+            $biUser = sprintf('{%d}', strlen($userId));
+            $result = $imsp->imspSend($biUser, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $imsp->imspReceive())) {
+
+                $result = $imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                        __FILE__, __LINE__);
+                return $result;
+           }
+        }
+
+        $result = $imsp->imspSend($userId . ' ', false, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Don't want to log the password! */
+        $logValue = $imsp->logEnabled;
+        $imsp->logEnabled = false;
+
+        /* Pass as {}? */
+        if (preg_match(IMSP_MUST_USE_LITERAL, $credentials)) {
+            $biPass = sprintf('{%d}', strlen($credentials));
+            $result = $imsp->imspSend($biPass, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $imsp->imspReceive())) {
+                $result = $imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                        __FILE__, __LINE__);
+                return $result;
+            }
+        }
+
+        $result = $imsp->imspSend($credentials, false, true);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Restore the logging boolean. */
+        $imsp->logEnabled = $logValue;
+
+        $server_response = $imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+
+        if ($server_response != 'OK') {
+            $result = $imsp->imspError('Login to IMSP host failed.', __FILE__, __LINE__);
+            return $result;
+        }
+
+        return $imsp;
+    }
+
+    /**
+     * Force a logout command to the imsp stream.
+     *
+     */
+    function logout()
+    {
+        $this->_imsp->logout();
+    }
+
+    /**
+     * Return the driver type
+     *
+     * @return string the type of this IMSP_Auth driver
+     */
+     function getDriverType()
+     {
+         return 'plaintext';
+     }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Book.php b/framework/Net_IMSP/IMSP/Book.php
new file mode 100644 (file)
index 0000000..576dc62
--- /dev/null
@@ -0,0 +1,1497 @@
+<?php
+
+require_once 'Net/IMSP/Auth.php';
+
+/**
+ * String of supported ACL rights.
+ */
+define('IMSP_ACL_RIGHTS', 'lrwcda');
+
+/**
+ * Net_IMSP_Book Class - provides api for dealing with IMSP
+ * address books.
+ *
+ * Required parameters:<pre>
+ *   'username'     Username to logon to IMSP server as.
+ *   'password'     Password for current user.
+ *   'auth_method'  The authentication method to use to login.
+ *   'server'       The hostname of the IMSP server.
+ *   'port'         The port of the IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Book.php,v 1.49 2009/10/01 23:43:46 mrubinsk Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Book {
+
+    /**
+     * Sort order.
+     *
+     * @var string
+     */
+    var $sort = 'ascend';
+
+    /**
+     * Net_IMSP object.
+     *
+     * @var Net_IMSP
+     */
+    var $_imsp;
+
+    /**
+     * Parameter list.
+     *
+     * @var array
+     */
+    var $_params;
+
+    /**
+     * Constructor function.
+     *
+     * @param array $params Hash containing IMSP parameters.
+     */
+    function Net_IMSP_Book($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Initialization function to be called after object is returned.
+     * This allows errors to occur and not break the script.
+     *
+     * @return mixed  True on success PEAR_Error on failure.
+     */
+    function init()
+    {
+        if (!isset($this->_imsp)) {
+            $auth = &Net_IMSP_Auth::singleton($this->_params['auth_method']);
+            $this->_imsp = $auth->authenticate($this->_params);
+        }
+
+        if (is_a($this->_imsp, 'PEAR_Error')) {
+            return $this->_imsp;
+        }
+        $this->_imsp->writeToLog('Net_IMSP_Book initialized.', __FILE__,
+                                  __LINE__, PEAR_LOG_DEBUG);
+        return true;
+    }
+
+    /**
+     * Returns an array containing the names of all the address books
+     * available to the logged in user.
+     *
+     * @return mixed Array of address book names or PEAR_Error.
+     */
+    function getAddressBookList()
+    {
+        $command_string = 'ADDRESSBOOK *';
+
+        $result = $this->_imsp->imspSend($command_string);
+        if (is_a($result,'PEAR_Error')) {
+           return $this->_imsp->imspError('Connection to IMSP host failed.',
+                                          __FILE__, __LINE__);
+        }
+
+        /* Iterate through the response and populate an array of
+         * address book names. */
+        $server_response = $this->_imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+        $abooks = array();
+
+        while (preg_match("/^\* ADDRESSBOOK/", $server_response)) {
+            /* If this is an ADDRESSBOOK response, then this will explode as so:
+             * [0] and [1] can be discarded
+             * [2] = attributes
+             * [3] = delimiter
+             * [4] = address book name
+             */
+
+            /* First, check for a {} */
+            if (preg_match(IMSP_OCTET_COUNT, $server_response, $tempArray)) {
+                $abooks[] = $this->_imsp->receiveStringLiteral($tempArray[2]);
+                /* Get the CRLF at end of ADDRESSBOOK response
+                 * that the {} does not include. */
+                $this->_imsp->receiveStringLiteral(2);
+            } else {
+                $parts = explode(' ', $server_response);
+                $numParts = count($parts);
+                $name = $parts[4];
+                $firstChar = substr($name, 0, 1);
+                if ($firstChar =="\"") {
+                    $name = ltrim($name, "\"");
+                    for ($i = 5; $i < $numParts; $i++) {
+                        $name .= ' ' . $parts[$i];
+                        $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
+                        if ($lastChar == "\"") {
+                            $name = rtrim($name, "\"");
+                            break;
+                        }
+                    }
+                }
+                $abooks[] = $name;
+            }
+            $server_response = $this->_imsp->imspReceive();
+        }
+
+        if ($server_response != 'OK') {
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+
+        $this->_imsp->writeToLog('ADDRESSBOOK command OK.', __FILE__, __LINE__,
+                                  PEAR_LOG_INFO);
+        return $abooks;
+    }
+
+    /**
+     * Returns an array containing the names that match $search
+     * critera in the address book named $abook.
+     *
+     * @param string $abook  Address book name to search.
+     * @param array $search  Search criteria in the form of
+     *                      'fieldName' => 'searchTerm' (may include * wild card).
+     *
+     * @return mixed Array of names of the entries that match or PEAR_Error.
+     */
+    function search($abook, $search)
+    {
+        //If no field => value pairs, assume we are searching name.
+        $criteria = array();
+        if (!is_array($search)) {
+            $criteria['name'] = $search;
+        } else {
+            $criteria = $search;
+        }
+
+        $result = $this->_imsp->imspSend('SEARCHADDRESS ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Do we need to send the abook name as {} ?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $result = $this->_imsp->imspSend($biBook, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $result = $this->_imsp->imspReceive();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            if (!preg_match("/^\+/", $result)) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__,__LINE__);
+            }
+        }
+
+        //Start parsing the search array.
+        $result = $this->_imsp->imspSend("$abook", false, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $count = count($criteria);
+        $current = 1;
+        foreach ($criteria as $search_field => $search) {
+            $result = $this->_imsp->imspSend(" $search_field ", false, false);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            // How about the search term as a {}.
+            if (preg_match(IMSP_MUST_USE_LITERAL, $search)) {
+                $biSearch = sprintf("{%d}", strlen($search));
+
+                $result = $this->_imsp->imspSend($biSearch, false, true);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                $result = $this->_imsp->imspReceive();
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                if (!preg_match("/^\+/", $result)) {
+                    return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                                   __FILE__, __LINE__);
+                }
+
+                $result = $this->_imsp->imspSend($search, false, $current == $count);
+                $current++;
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            } else {
+                // Only send the CrLf if this is the last field/search atom.
+                $result = $this->_imsp->imspSend('"' . $search . '"', false, $current == $count);
+                $current++;
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+        }
+
+        // Get the response.
+        $server_response = $this->_imsp->imspReceive();
+        $abookNames = Array();
+
+        while (preg_match("/^\* SEARCHADDRESS/", $server_response)) {
+            $chopped_response =
+                preg_replace("/^\* SEARCHADDRESS/", '', $server_response);
+
+            // Remove any lingering white space in front only.
+            $chopped_response = ltrim($chopped_response);
+
+            // Get rid of any lingering quotes.
+            $temp = preg_replace("/\"/", '', $chopped_response);
+
+            if (preg_match("/({)([0-9]{1,})(\}$)/", $temp, $tempArray)) {
+                $dataSize = $tempArray[2];
+                $temp = $this->_imsp->receiveStringLiteral($dataSize);
+
+                /* Get the CRLF since {} does not include it. */
+                $this->_imsp->receiveStringLiteral(2);
+            }
+
+            $abookNames[] = $temp;
+
+            // Get the next response line from the server.
+            $server_response = $this->_imsp->imspReceive();
+        }
+
+        // Should check for OK or BAD here just to be certain.
+        switch ($server_response) {
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.' .  ": $command_text",
+                                           __FILE__, __LINE__);
+
+        case 'NO':
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.' . ": " . $this->_imsp->lastRawError,
+                                           __FILE__, __LINE__);
+        }
+
+        /* This allows for no results */
+        if (count($abookNames) < 1) {
+            return $abookNames;
+        }
+
+        $this->_imsp->writeToLog('SEARCHADDRESS command OK', __FILE__, __LINE__,
+                                 PEAR_LOG_INFO);
+
+        // Determine the sort direction and perform the sort.
+        switch ($this->sort) {
+        case 'ascend':
+            sort($abookNames);
+            break;
+
+        case 'descend':
+            rsort($abookNames);
+            break;
+        }
+
+        return $abookNames;
+    }
+
+    /**
+     * Returns an associative array of a single address book entry.
+     * Note that there will always be a 'name' field.
+     *
+     * @param string $abook       Name of the address book to search.
+     * @param string $entryName  'name' attribute of the entry to retrieve
+     *
+     * @return mixed Array containing entry or PEAR_Error on failure / no match.
+     */
+    function getEntry($abook, $entryName)
+    {
+        $result = $this->_imsp->imspSend('FETCHADDRESS ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $result = $this->_imsp->imspSend($biBook, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+        }
+
+        $result = $this->_imsp->imspSend("$abook ", false, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $entryName)) {
+            $biName = sprintf("{%d}", strlen($entryName));
+
+            $result = $this->_imsp->imspSend($biName, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+            $result = $this->_imsp->imspSend($entryName, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $result = $this->_imsp->imspSend("\"$entryName\"", false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+        case 'NO':
+            return $this->_imsp->imspError('No entry in this address book matches your query.', __FILE__, __LINE__);
+        }
+
+        // Get the data in an associative array.
+        $entry = $this->_parseFetchAddressResponse($server_response);
+
+        //Get the next server response -- this *should* be the OK response.
+        $server_response = $this->_imsp->imspReceive();
+
+        if ($server_response != 'OK') {
+            // Unexpected response throw error but still continue on.
+            $this->_imsp->imspError('Did not receive the expected response from the server.',__FILE__, __LINE__);
+        }
+
+        $this->_imsp->writeToLog('FETCHADDRESS completed OK', __FILE__, __LINE__,
+                                  PEAR_LOG_INFO);
+        return $entry;
+    }
+
+    /**
+     * Creates a new address book.
+     *
+     * @param string $abookName FULLY QUALIFIED name such 'jdoe.clients' etc...
+     *
+     * @return mixed True on success / PEAR_Error on failure.
+     */
+    function createAddressBook($abookName)
+    {
+        $command_text = 'CREATEADDRESSBOOK ';
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abookName)) {
+            $biBook = sprintf("{%d}", strlen($abookName));
+
+            $result = $this->_imsp->imspSend($command_text . $biBook, true, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            } else {
+                $result = $this->_imsp->imspSend($abookName, false, true);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+        } else {
+            $result = $this->_imsp->imspSend($command_text . $abookName,
+                                             true, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+
+        switch ($server_response) {
+        case 'OK':
+            $this->_imsp->writeToLog('CREATEADDRESSBOOK completed OK', __FILE__,
+                                     __LINE__, PEAR_LOG_INFO);
+            return true;
+
+        case 'NO':
+            // Could not create abook.
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+
+        default:
+            // Something unexpected.
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Deletes an address book completely!
+     *
+     * @param string $abookName Name of address book to delete.
+     *
+     * @return mixed true on success / PEAR_Error on failure
+     */
+    function deleteAddressBook($abookName)
+    {
+        $command_text = 'DELETEADDRESSBOOK ';
+
+        // Check need for {}.
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abookName)) {
+            $biBook = sprintf("{%d}", strlen($abookName));
+
+            $result = $this->_imsp->imspSend($command_text . $biBook, true, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            } else {
+                $result = $this->_imsp->imspSend($abookName, false, true);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+        } else {
+            $result = $this->_imsp->imspSend($command_text . $abookName, true, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+
+        switch ($server_response) {
+        case 'OK':
+            $this->_imsp->writeToLog('DELETEADDRESSBOOK completed OK', __FILE__,
+                                     __LINE__, PEAR_LOG_INFO);
+            return true;
+
+        case 'NO':
+            // Could not DELETE abook.
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+
+        default:
+            // Something unexpected.
+            return $this->_imsp->imspError('Did not receive the expected response from the server.', __FILE__,
+                                           __LINE__);
+        }
+    }
+
+    /**
+     * Renames an address book.
+     *
+     * @param string $abookOldName Old name.
+     * @param string $abookNewName New address book name.
+     *
+     * @return mixed True / PEAR_Error
+     */
+    function renameAddressBook($abookOldName, $abookNewName)
+    {
+        $result = $this->_imsp->imspSend('RENAMEADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abookOldName)) {
+            $biOldName = sprintf("{%d}", strlen($abookOldName));
+
+            $result = $this->_imsp->imspSend($biOldName, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $this->_imsp->imspReceive();
+        }
+
+        $result = $this->_imsp->imspSend("$abookOldName ", false, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abookNewName)) {
+            $biNewName = sprintf("{%d}", strlen($abookNewName));
+
+            $result = $this->_imsp->imspSend($biNewName, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+               return $result;
+            }
+
+            $this->_imsp->imspReceive();
+        }
+
+        // CRLF since last part.
+        $result = $this->_imsp->imspSend($abookNewName, false, true);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Get server response.
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'NO':
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            // Syntax problem.
+            return $this->_imsp->imspError('The IMSP server did not understand your request.');
+
+        case 'OK':
+            $this->_imsp->writeToLog("Address book $abookOldName successfully
+                                      changed to $abookNewName", __FILE__,
+                                      __LINE__, PEAR_LOG_INFO);
+            return true;
+
+        default:
+            return $this->_imsp->imspError('Did not receive the expected response from the server.', __FILE__,
+                                           __LINE__);
+        }
+    }
+
+    /**
+     * Adds an address book entry to an address book.
+     *
+     * @param string $abook     Name of address book to add entry to.
+     * @param array $entryInfo  Address book entry information -
+     *                          there MUST be a field 'name' containing the
+     *                          entry name.
+     *
+     * @return mixed True on success / PEAR_Error on failure.
+     */
+    function addEntry($abook, $entryInfo)
+    {
+        $command_text = '';
+
+        if (getType($entryInfo) != 'array') {
+            return $this->_imsp->imspError(IMSP_BAD_ARGUMENT, __FILE__, __LINE__);
+        }
+
+        // Lock the entry if it already exists.
+        $result = $this->lockEntry($abook, $entryInfo['name']);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->_imsp->imspSend('STOREADDRESS ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Take care of the name.
+        $entryName = $entryInfo['name'];
+
+        // {} for book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $result = $this->_imsp->imspSend($biBook, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $this->_imsp->imspReceive();
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        // Do we need {} for entry name as well?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $entryName)) {
+            $biname = sprintf("{%d}", strlen($entryName));
+            $this->_imsp->imspSend($biname, false, true);
+            $this->_imsp->imspReceive();
+            $this->_imsp->imspSend($entryName, false, false);
+        } else {
+            $this->_imsp->imspSend("\"$entryName\" ", false, false);
+        }
+
+        while (list($key, $value) = each($entryInfo)) {
+            // Do not sent the key name 'name'.
+            if ($key != 'name') {
+                // Protect from extraneous white space
+                $value = trim($value);
+
+                // For some reason, tabs seem to break this.
+                $value = preg_replace("/\t/", "\n\r", $value);
+
+                // Check to see if we need {}
+                if (preg_match(IMSP_MUST_USE_LITERAL, $value)) {
+                    $command_text .= $key . sprintf(" {%d}", strlen($value));
+
+                    $this->_imsp->imspSend($command_text, false, true);
+                    $server_response = $this->_imsp->imspReceive();
+                    $command_text = ''; //Clear the command_text buffer
+
+                    if (!preg_match("/^\+/",
+                                    $server_response)) {
+                        return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                                       __FILE__, __LINE__);
+                    }
+
+                    // Send the string of octets and be sure NOT to
+                    // end with CRLF.
+                    $this->_imsp->imspSend($value, false, false);
+
+                } else {
+                    // If we are here, then we do not need to send a
+                    // {}.
+                    $value = "\"" . $value . "\"";
+                    $command_text .= $key . ' ' . $value . ' ';
+                }
+            }
+        }
+
+        // Send anything that is left of the command.
+        $this->_imsp->imspSend($command_text, false, true);
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'NO':
+            //Sorry...cannot do it.
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            //Sorry...did not understand you
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__,
+                                           __LINE__);
+        }
+
+        if ($server_response != 'OK') {
+            // Cyrus-IMSP server sends a FETCHADDRESS Response here.
+            // Do others?     This was not in the RFC.
+            $dummy_array =
+                $this->_parseFetchAddressResponse($server_response);
+
+            $server_response = $this->_imsp->imspReceive();
+
+            switch ($server_response) {
+            case 'NO':
+                return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+            case 'BAD':
+                return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__,
+                                               __LINE__);
+            case 'OK':
+                $this->_imsp->writeToLog('STOREADDRESS Completed successfully.',
+                                         __FILE__, __LINE__, PEAR_LOG_INFO);
+
+                //we were successful...so release the lock on the entry
+                if (!$this->unlockEntry($abook, $entryInfo['name'])) {
+                    //could not release lock
+                    return $this->_imsp->imspError('That address book entry is locked or read only for the current user.', __FILE__,
+                                                   __LINE__);
+                }
+
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Deletes an abook entry.
+     *
+     * @param string $abook     Name of address book containing entry.
+     * @param string $bookEntry Name of entry to delete.
+     *
+     * @return mixed True on success / PEAR_Error on failure.
+     */
+    function deleteEntry($abook, $bookEntry)
+    {
+        // Start the command.
+        $result = $this->_imsp->imspSend('DELETEADDRESS ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Need {} for book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        //How bout for the entry name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $bookEntry)) {
+            $biEntry = sprintf("{%d}", strlen($bookEntry));
+
+            $this->_imsp->imspSend($biEntry, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        } else {
+            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
+        }
+
+        $this->_imsp->imspSend($bookEntry, false, true);
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'NO':
+            //Sorry..cannot do it
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+        case 'BAD':
+            //Do not know what your talking about
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__,
+                                           __LINE__);
+        case 'OK':
+            $this->_imsp->writeToLog('DELETE Completed successfully.', __FILE__,
+                                     __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+    }
+
+    /**
+     * Attempts to acquire a semaphore on the address book entry.
+     *
+     * @param string $abook     Address book name
+     * @param string $bookEntry Name of entry to lock
+     *
+     * @return mixed true or array on success and PEAR_Error on failure
+     *               (server depending)
+     */
+    function lockEntry($abook, $bookEntry)
+    {
+        $result = $this->_imsp->imspSend('LOCK ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Do we need a string literal?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        // What about the entry name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $bookEntry)) {
+            $biEntry = sprintf("{%d}", strlen($bookEntry));
+
+            $this->_imsp->imspSend($biEntry, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+            $this->_imsp->imspSend($bookEntry, false, true);
+
+        } else {
+            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
+            $this->_imsp->imspSend("$bookEntry", false, true);
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+
+        do {
+
+            switch ($server_response) {
+
+            case 'NO':
+                return $this->_imsp->imspError('That address book entry is locked or read only for the current user.',
+                                               __FILE__, __LINE__);
+            case 'BAD':
+                return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__,
+                                               __LINE__);
+            }
+
+            //Check to see if this is a FETCHADDRESS resonse
+            $dummy = $this->_parseFetchAddressResponse($server_response);
+
+            if ($dummy) {
+                $server_response = $this->_imsp->imspReceive();
+            }
+
+        } while ($server_response != 'OK');
+
+        $this->_imsp->writeToLog("LOCK ADDRESSBOOK on $abook $bookEntry OK",
+                                 __FILE__, __LINE__, PEAR_LOG_INFO);
+
+        // Return either true or the FETCHADDRESS response if it exists.
+        if (!$dummy) {
+            return true;
+        } else {
+            return $dummy;
+        }
+    }
+
+    /**
+     * Unlocks a previously locked address book.
+     *
+     * @param string $abook     Name of address book containing locked entry.
+     * @param string $bookEntry Name of entry to unlock.
+     *
+     * @return mixed True on success, PEAR_Error on failure.
+     */
+    function unlockEntry($abook, $bookEntry)
+    {
+        // Start sending command.
+        $result = $this->_imsp->imspSend('UNLOCK ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // {} for book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        //How bout for entry name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $bookEntry)) {
+            $biEntry=sprintf("{%d}", strlen($bookEntry));
+            $this->_imsp->imspSend($biEntry, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+            $this->_imsp->imspSend($bookEntry, false, true);
+
+        } else {
+            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
+            $this->_imsp->imspSend("$bookEntry", false, true);
+        }
+
+        $response = $this->_imsp->imspReceive();
+
+        switch ($response) {
+        case 'NO':
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+
+        case 'OK':
+            $this->_imsp->writeToLog("UNLOCK ADDRESSBOOK on $abook $bookEntry OK",
+                                     __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+        }
+    }
+
+    /**
+     * Access Control List (ACL)  Methods.
+     *
+     * The following characters are recognized ACL characters: lrwcda
+     * l - "lookup"  (see the name and existence of the address book)
+     * r - "read"    (search and retrieve addresses from address book)
+     * w - "write"   (create/edit new address book entries - not delete)
+     * c - "create"  (create new address books under the current address book)
+     * d - "delete"  (delete entries or entire book)
+     * a - "admin"   (set ACL lists for this address book - usually only
+     *               allowed for the owner of the address book)
+     *
+     * examples:
+     *  "lr" would be read only for that user
+     *  "lrw" would be read/write
+     */
+
+    /**
+     * Sets an Access Control List for an abook.
+     *
+     * @param string $abook Name of address book.
+     * @param string $ident Name of user for this acl.
+     * @param string $acl   acl for this user/book.
+     *
+     * @return mixed True on success / PEAR_Error on failure.
+     */
+    function setACL($abook, $ident, $acl)
+    {
+        // Verify that $acl looks good.
+        if (preg_match("/[^" . IMSP_ACL_RIGHTS . "]/", $acl)) {
+            //error...acl list contained unrecoginzed options
+            return $this->_imsp->imspError(IMSP_BAD_ARGUMENT, __FILE__, __LINE__);
+        }
+
+        // Begin sending command.
+        $result = $this->_imsp->imspSend('SETACL ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // {} for book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        // {} for ident?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $ident)) {
+            $biIdent = sprintf("{%d}", strlen($ident));
+            $this->_imsp->imspSend($biIdent, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend("$ident ", false, false);
+
+        // Now finish up with the actual ACL.
+        $this->_imsp->imspSend($acl, false, true);
+        $response = $this->_imsp->imspReceive();
+
+        switch ($response) {
+        case 'NO':
+            // Could not set ACL.
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.',__FILE__, __LINE__);
+
+        case 'BAD':
+            // Bad syntax.
+            return $this->_imsp->imspError('The IMSP server did not understand your request.',__FILE__, __LINE__);
+
+        case 'OK':
+            $this->_imsp->writeToLog("ACL set for $ident on $abook",
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return true;
+
+        default:
+            // Do not know why we would make it down here.
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Retrieves an address book's ACL.
+     *
+     * @param string $abook Name of address book to retrieve acl for.
+     *
+     * @return mixed array containing acl for every user with access to
+     *                     address book or PEAR_Error on failure.
+     */
+    function getACL($abook)
+    {
+        $result = $this->_imsp->imspSend('GETACL ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // {} for book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biName = sprintf("{%d}", strlen($abook));
+            $this->_imsp->imspSend($biName, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+        }
+
+        $this->_imsp->imspSend($abook, false, true);
+
+        // Get results.
+        $response = $this->_imsp->imspReceive();
+
+        switch ($response) {
+        case 'NO':
+            // Could not complete?
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            // Do not know what you said!
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+        }
+
+        // If we are here, we need to receive the * ACL Responses.
+        do {
+            /* Get an array of responses.
+             * The [3] element should be the address book name
+             * [4] and [5] will be user/group name and permissions */
+
+            //the book name might be a literal
+            if (preg_match(IMSP_OCTET_COUNT, $response, $tempArray)) {
+                $data = $this->_imsp->receiveStringLiteral($tempArray[2]);
+                //Get the rest
+                $response = $this->_imsp->imspReceive();
+            }
+
+            $parts = explode(' ', $response);
+
+            // Push the array if book was a literal
+            if ($data) {
+                array_unshift($parts, ' ', ' ', ' ', ' ');
+            }
+            // Address book name quoted?
+            $numParts = count($parts);
+            $name = $parts[3];
+            $firstACLIdx = 4;
+            $firstChar = substr($name, 0, 1);
+            if ($firstChar == "\"") {
+                for ($i = 4; $i < $numParts; $i++) {
+                    $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
+                    $firstACLIdx++;
+                    if ($lastChar == "\"") {
+                        break;
+                    }
+                }
+            }
+
+            for ($i = $firstACLIdx; $i < count($parts); $i += 2) {
+                $results[$parts[$i]] = $parts[$i+1];
+            }
+
+            $response = $this->_imsp->imspReceive();
+
+        } while (preg_match("/^\* ACL ADDRESSBOOK/", $response));
+
+        // Hopefully we can receive an OK response here
+        if ($response != 'OK') {
+            // Some weird problem
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+
+        $this->_imsp->writeToLog("GETACL on $abook completed.", __FILE__,
+                                 __LINE__, PEAR_LOG_INFO);
+        return $results;
+
+    }
+
+    /**
+     * Deletes an ACL entry for an address book.
+     *
+     * @param string $abook Name of the address book.
+     * @param string $ident Name of entry to remove acl for.
+     *
+     * @return mixed true on success, PEAR_Error on failure.
+     */
+    function deleteACL($abook, $ident)
+    {
+        $result = $this->_imsp->imspSend('DELETEACL ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Do we need literal for address book name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend("$abook ", false, false);
+
+        // Literal for ident name?
+        if (preg_match(IMSP_MUST_USE_LITERAL, $ident)) {
+            $biIdent = sprintf("{%d}", strlen($ident));
+            $this->_imsp->imspSend($biIdent, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+            $this->_imsp->imspSend($ident, false, true);
+        } else {
+            $this->_imsp->imspSend("\"$ident\"", false, true);
+        }
+
+        // Get results.
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'NO':
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+
+        case 'OK':
+            $this->_imsp->writeToLog("DELETED ACL for $ident on $abook",
+                                      __FILE__, __LINE__, PEAR_LOG_INFO);
+            return true;
+
+        default:
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Returns an ACL string containing the rights for the current user
+     *
+     * @param string $abook Name of address book to retrieve acl.
+     *
+     * @return mixed acl of current user or PEAR_Error on failure.
+     */
+    function myRights($abook)
+    {
+        $data = '';
+        $result = $this->_imsp->imspSend('MYRIGHTS ADDRESSBOOK ', true, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (preg_match(IMSP_MUST_USE_LITERAL, $abook)) {
+            $biBook = sprintf("{%d}", strlen($abook));
+            $this->_imsp->imspSend($biBook, false, true);
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+
+        }
+
+        $this->_imsp->imspSend($abook, false, true);
+        $server_response = $this->_imsp->imspReceive();
+
+        switch ($server_response) {
+        case 'NO':
+            return $this->_imsp->imspError('IMSP server is unable to perform your request.', __FILE__, __LINE__);
+
+        case 'BAD':
+            return $this->_imsp->imspError('The IMSP server did not understand your request.', __FILE__, __LINE__);
+        }
+
+        if (!preg_match("/^\* MYRIGHTS ADDRESSBOOK/", $server_response)) {
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+
+        // {} for the abook name?
+        if (preg_match(IMSP_OCTET_COUNT, $server_response, $tempArray)) {
+            $data = $this->_imsp->receiveStringLiteral($tempArray[2]);
+            // Get the rest.
+            $server_response = $this->_imsp->imspReceive();
+        }
+
+        $parts = explode(' ', $server_response);
+
+        // Push the array if we had a {}
+        if ($data) {
+            array_unshift($parts, ' ', ' ', ' ', ' ');
+        }
+
+        // Quoted address book name?
+        $numParts = count($parts);
+        $name = $parts[3];
+        $firstACLIdx = 4;
+        $firstChar = substr($name, 0, 1);
+        if ($firstChar == "\"") {
+            for ($i = 4; $i < $numParts; $i++) {
+                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
+                $firstACLIdx++;
+                if ($lastChar == "\"") {
+                    break;
+                }
+            }
+        }
+
+        $acl = $parts[$firstACLIdx];
+        $server_response = $this->_imsp->imspReceive();
+
+        if ($server_response != 'OK') {
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        } else {
+            $this->_imsp->writeToLog("MYRIGHTS on $abook completed.",
+                                      __FILE__, __LINE__, PEAR_LOG_INFO);
+            return $acl;
+        }
+    }
+
+    /**
+     * Sets the log information in the Net_IMSP object.
+     *
+     * @param  array  Log parameters.
+     *
+     * @return mixed  True on success PEAR_Error on failure.
+     */
+    function setLogger($params)
+    {
+        if (isset($this->_imsp)) {
+            return $this->_imsp->setLogger($params);
+        } else {
+            return PEAR::raiseError(_("The IMSP log could not be initialized."));
+        }
+    }
+
+    /**
+     * Parses a IMSP fetchaddress response text string into
+     * key-value pairs
+     *
+     * @access private
+     * @param  string  $server_response The raw fetchaddress response.
+     *
+     * @return array   Address book entry information as key=>value pairs.
+     */
+    function _parseFetchAddressResponse($server_response)
+    {
+        $abook = '';
+
+        if (!preg_match("/^\* FETCHADDRESS /", $server_response)) {
+            $this->_imsp->writeToLog('[ERROR] Did not receive a FETCHADDRESS response from server.',
+                                     __FILE__, __LINE__, PEAR_LOG_ERR);
+
+            $this->_imsp->exitCode = 'Did not receive the expected response from the server.';
+            return false;
+        }
+
+        /* NOTES
+         * Parse out the server response string
+         *
+         * After choping off the server command response tags and
+         * explode()'ing the server_response string
+         * the $parts array contains the chunks of the server returned data.
+         *
+         * The predifined 'name' field starts in $parts[1].
+         * The server should return any single item of data
+         * that contains spaces within it as a double quoted string.
+         * So we can interpret the existence of a double quote at the beginning
+         * of a chunk to mean that the next chunk(s) are part of
+         * the same value.  A double quote at the end of a chunk signifies the
+         * end of that value and the chunk following that can be interpreted
+         * as a key name.
+         *
+         * We also need to watch for the server returning a {} response for the
+         * value of the key as well. */
+
+        // Was the address book name a  {}?
+        if (preg_match("/(^\* FETCHADDRESS )({)([0-9]{1,})(\}$)/",
+                       $server_response, $tempArray)) {
+            $abook = $this->_imsp->receiveStringLiteral($tempArray[3]);
+            $chopped_response = trim($this->_imsp->imspReceive());
+        } else {
+            // Take off the stuff from the beginning of the response
+            $chopped_response = trim(preg_replace("/^\* FETCHADDRESS /",
+                                                  '', $server_response));
+        }
+
+        $parts = explode(' ', $chopped_response);
+        /* If addres book was sent as a {} then we must 'push' a blank
+         * value to the start of this array so the rest of the routine
+         * will work with the correct indexes. */
+         if (!empty($abook)) {
+            array_unshift($parts, ' ');
+        }
+
+        // Was the address book name quoted?
+        $numOfParts = count($parts);
+        $name = $parts[0];
+        $firstNameIdx = 1;
+        $firstChar = substr($name, 0, 1);
+        if ($firstChar =="\"") {
+            for ($i = 1; $i < $numOfParts; $i++) {
+                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
+                $firstNameIdx++;
+                if ($lastChar == "\"") {
+                    break;
+                }
+            }
+        }
+
+        // Now start working on the entry name
+        $name = $parts[$firstNameIdx];
+        $firstChar = substr($name,0,1);
+
+        // Check to see if the first char of the name string is a double quote
+        // so we know if we have to extract more of the name.
+        if ($firstChar == "\"") {
+            $name = ltrim($name, "\"");
+            for ($i = $firstNameIdx + 1; $i < $numOfParts; $i++) {
+                $name .=  ' ' . $parts[$i];
+                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1,1);
+                if ($lastChar == "\"") {
+                    $name = rtrim($name, "\"");
+                    $nextKey = $i + 1;
+                    break;
+                }
+            }
+
+        // Check for {}
+        } elseif (preg_match('/\{(\d+)\}/', $name, $matches)) {
+            $name = $this->_imsp->receiveStringLiteral($matches[1]);
+            $response=$this->_imsp->imspReceive();
+            $parts = explode(' ', $response);
+            $numOfParts = count($parts);
+            $nextKey = 0;
+        } else {
+            // If only one chunk for 'name' then we just have to point
+            // to the next chunk in the array...which will hopefully
+            // be '2'
+            $nextKey = $firstNameIdx + 1;
+        }
+
+        $lastChar = '';
+        $entry['name'] = $name;
+
+        // Start parsing the rest of the response.
+        for ($i = $nextKey; $i < $numOfParts; $i += 2) {
+            $key = $parts[$i];
+            /* Check for {} */
+            if (@preg_match(IMSP_OCTET_COUNT, $parts[$i+1], $tempArray)) {
+                $server_data = $this->_imsp->receiveStringLiteral($tempArray[2]);
+                $entry[$key] = $server_data;
+
+                /* Read any remaining data from the stream and reset
+                 * the counter variables so the loop will continue
+                 * correctly. Note we set $i  to -2 because it will
+                 * be incremented by 2 before the loop will run again */
+                $parts = $this->_imsp->getServerResponseChunks();
+                $i = -2;
+                $numOfParts = count($parts);
+            } else {
+                // Not a string literal response
+                @$entry[$key] = $parts[$i + 1];
+                 // Check to see if the value started with a double
+                 // quote.  We also need to check if the last char is a
+                 // quote to make sure we REALLY have to check the next
+                 // elements for a closing quote.
+                if ((@substr($parts[$i + 1], 0, 1) == '"') &&
+                    (substr($parts[$i + 1],
+                     strlen($parts[$i + 1]) - 1, 1) != '"')) {
+
+                    do {
+                        $nextElement = $parts[$i+2];
+
+                        // Was this element the last one?
+                        $lastChar = substr($nextElement,
+                                           strlen($nextElement) - 1, 1);
+                        $entry[$key] .= ' ' . $nextElement;
+
+                        // NOW, we can check the lastChar.
+                        if ($lastChar == '"') {
+                            $done = true;
+                            $i++;
+                        } else {
+                            // Check to see if the next element is the
+                            // last one. If so, the do loop will terminate.
+                            $done = false;
+                            $lastChar = substr($parts[$i+3],
+                                               strlen($parts[$i+3]) - 1,1);
+                            $i++;
+                        }
+                    } while ($lastChar != '"');
+
+                    // Do we need to add the final element, or were
+                    // there only two total?
+                    if (!$done) {
+                        $nextElement = $parts[$i+2];
+                        $entry[$key] .= ' ' . $nextElement;
+                        $i++;
+                    }
+
+                    // Remove the quotes sent back to us from the server.
+                    if (substr($entry[$key], 0, 1) == '"') {
+                        $entry[$key] = substr($entry[$key], 1,
+                                              strlen($entry[$key]) - 2);
+                    }
+
+                    if (substr($entry[$key],
+                               strlen($entry[$key]) - 1, 1) == '"') {
+
+                        $entry[$key] = substr($entry[$key], 0,
+                                              strlen($entry[$key]) - 2);
+                    }
+                } elseif ((@substr($parts[$i + 1], 0, 1) == '"') && 
+                          (substr($parts[$i + 1], -1, 1) == '"')) {
+                    // Remove the quotes sent back to us from the server.
+                    if (substr($entry[$key], 0, 1) == '"') {
+                        $entry[$key] = substr($entry[$key], 1,
+                                              strlen($entry[$key]) - 2);
+                    }
+
+                    if (substr($entry[$key], -1, 1) == '"') {
+                        $entry[$key] = substr($entry[$key], 0,
+                                              strlen($entry[$key]) - 2);
+                    }
+                }
+            }
+        }
+        return $entry;
+    }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Options.php b/framework/Net_IMSP/IMSP/Options.php
new file mode 100644 (file)
index 0000000..4ba9f40
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+
+require_once 'Net/IMSP/Auth.php';
+
+/**
+ * Net_IMSP_Options Class - provides an interface to IMSP server-based
+ * options storage.
+ *
+ * Required parameters:<pre>
+ *   'username'     Username to logon to IMSP server as.
+ *   'password'     Password for current user.
+ *   'auth_method'  The authentication method to use to login.
+ *   'server'       The hostname of the IMSP server.
+ *   'port'         The port of the IMSP server.</pre>
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Options.php,v 1.23 2009/01/06 17:49:34 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Options {
+
+    /**
+     * Net_IMSP object.
+     *
+     * @var Net_IMSP
+     */
+    var $_imsp;
+
+    /**
+     * Parameter list.
+     *
+     * @var array
+     */
+    var $_params;
+
+    /**
+     * Constructor function.
+     *
+     * @param array $params  Hash containing IMSP parameters.
+     */
+    function Net_IMSP_Options($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Initialization function to be called after object is returned.
+     * This allows errors to occur and not break the script.
+     *
+     * @return mixed  True on success PEAR_Error on failure.
+     */
+    function init()
+    {
+        if (!isset($this->_imsp)) {
+            $auth = &Net_IMSP_Auth::singleton($this->_params['auth_method']);
+            $this->_imsp = $auth->authenticate($this->_params);
+        }
+
+        if (is_a($this->_imsp, 'PEAR_Error')) {
+            return $this->_imsp;
+        }
+
+        $this->_imsp->writeToLog('Net_IMSP_Options initialized.', __FILE__,
+                                 __LINE__, PEAR_LOG_DEBUG);
+        return true;
+    }
+
+    /**
+     * Function sends a GET command to IMSP server and retrieves values.
+     *
+     * @param  string $optionName Name of option to retrieve. Accepts '*'
+     *                            as wild card.
+     *
+     * @return mixed  Associative array containing option=>value pairs or
+     *                PEAR_Error.
+     */
+    function get($optionName)
+    {
+        $options = array();
+        $result = $this->_imsp->imspSend("GET $optionName", true, true);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        }
+
+        while (preg_match("/^\* OPTION/", $server_response)) {
+            /* First, check for a {}. */
+            if (preg_match(IMSP_OCTET_COUNT, $server_response, $tempArray)) {
+                $temp = explode(' ', $server_response);
+                $options[$temp[2]] = $this->_imsp->receiveStringLiteral($tempArray[2]);
+                $this->_imsp->imspReceive();
+            } else {
+                $temp = explode(' ', $server_response);
+                $options[$temp[2]] = trim($temp[3]);
+                $i = 3;
+                $lastChar = "";
+                $nextElement = trim($temp[3]);
+
+                /* Was the value quoted and spaced? */
+                if ((substr($nextElement,0,1) == '"') &&
+                    (substr($nextElement,strlen($nextElement) - 1, 1) != '"')) {
+                    do {
+                        $nextElement = $temp[$i+1];
+                        $lastChar = substr($nextElement,
+                                           strlen($nextElement) - 1, 1);
+                        $options[$temp[2]] .= ' ' . $nextElement;
+                        if ($lastChar == '"') {
+                            $done = true;
+                        } else {
+                            $done = false;
+                            $lastChar = substr($temp[$i+2],
+                                               strlen($temp[$i+2]) - 1, 1);
+                            $i++;
+                        }
+
+                    } while ($lastChar != '"');
+
+                    if (!$done) {
+                        $nextElement = $temp[$i+1];
+                        $options[$temp[2]] .= ' ' . $nextElement;
+                    }
+                }
+            }
+            $server_response = $this->_imsp->imspReceive();
+            if (is_a($server_response, 'PEAR_Error')) {
+                return $server_response;
+            }
+        }
+
+        if ($server_response != 'OK') {
+            return $this->_imsp->imspError('Did not receive the expected response from the server.',
+                                           __FILE__, __LINE__);
+        }
+
+        $this->_imsp->writeToLog('GET command OK.', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $options;
+    }
+
+    /**
+     * Function sets an option value on the IMSP server.
+     *
+     * @param string $optionName  Name of option to set.
+     * @param string $optionValue Value to assign.
+     *
+     * @return mixed True or PEAR_Error.
+     */
+    function set($optionName, $optionValue)
+    {
+        /* Send the beginning of the command. */
+        $result = $this->_imsp->imspSend("SET $optionName ", true, false);
+
+        /* Send $optionValue as a literal {}? */
+        if (preg_match(IMSP_MUST_USE_LITERAL, $optionValue)) {
+            $biValue = sprintf("{%d}", strlen($optionValue));
+            $result = $this->_imsp->imspSend($biValue, false, true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!preg_match("/^\+/",
+                            $this->_imsp->imspReceive())) {
+                return $this->_imsp->imspError('Did not receive expected command continuation response from IMSP server.',
+                                               __FILE__, __LINE__);
+            }
+        }
+
+        /* Now send the rest of the command. */
+        $result = $this->_imsp->imspSend($optionValue, false, true);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $server_response = $this->_imsp->imspReceive();
+
+        if (is_a($server_response, 'PEAR_Error')) {
+            return $server_response;
+        } elseif ($server_response != 'OK') {
+            return $this->_imsp->imspError('The option could not be set on the IMSP server.', __FILE__, __LINE__);
+        }
+
+        $this->_imsp->writeToLog('SET command OK.', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return true;
+    }
+
+    /**
+     * Sets the log information in the Net_IMSP object.
+     *
+     * @param array $params  The log parameters.
+     *
+     * @return mixed  True on success PEAR_Error on failure.
+     */
+    function setLogger($params)
+    {
+        if (isset($this->_imsp)) {
+            return $this->_imsp->setLogger($params);
+        } else {
+            return $this->_imsp->imspError('The IMSP log could not be initialized.');
+        }
+    }
+
+    function logout()
+    {
+        $this->_imsp->logout();
+    }
+
+}
diff --git a/framework/Net_IMSP/IMSP/Utils.php b/framework/Net_IMSP/IMSP/Utils.php
new file mode 100644 (file)
index 0000000..7c319bc
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+require_once 'Net/IMSP.php';
+/**
+ * Net_IMSP_Utils::
+ *
+ * $Horde: framework/Net_IMSP/IMSP/Utils.php,v 1.30 2009-11-22 18:15:21 slusarz Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Net_IMSP
+ */
+class Net_IMSP_Utils {
+
+    /**
+     * Utility function to retrieve the names of all the address books
+     * that the user has access to, along with the acl for those
+     * books.  For information about the $serverInfo array see
+     * turba/config/sources.php as this is the cfgSources[] entry for
+     * the address books.
+     *
+     * @param array $serverInfo  Information about the server
+     *                           and the current user.
+     *
+     * @return array  Information about all the address books or PEAR_Error.
+     */
+    function getAllBooks($serverInfo)
+    {
+        $foundDefault = false;
+        $results = array();
+        $imsp = &Net_IMSP::singleton('Book', $serverInfo['params']);
+        $result = $imsp->init();
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $books = $imsp->getAddressBookList();
+        if (is_a($books, 'PEAR_Error')) {
+            return $books;
+        }
+        $bCount = count($books);
+        for ($i = 0; $i < $bCount; $i++) {
+            $newBook = $serverInfo;
+            if ($books[$i] != $serverInfo['params']['username']) {
+                $newBook['title'] = $books[$i];
+                // We need the 'name' param to store the IMSP
+                // address book name when not using shares (for BC).
+                $newBook['params']['name'] = $books[$i];
+                $newBook['params']['is_root'] = false;
+                $newBook['params']['my_rights'] = $imsp->myRights($books[$i]);
+            } else {
+                $foundDefault = true;
+                $newBook['params']['my_rights'] = $imsp->myRights($books[$i]);
+            }
+            $results[] = $newBook;
+        }
+
+        /* If there is no default address book (named username) then we should create one. */
+        if (!$foundDefault) {
+            $result = $imsp->createAddressBook($serverInfo['params']['username']);
+            if (is_a($result, 'PEAR_Error')) {
+                return PEAR::raiseError('Login to IMSP host failed.' .
+                                        ': Default address book is missing and could not be created.');
+            }
+        }
+        return $results;
+    }
+
+    /**
+     * Utility function to make it easier for client applications to delete
+     * address books without having to create imsp drivers.  The $source array
+     * is a horde/turba style $cfgSources entry for the address book being
+     * deleted.
+     *
+     * @param array $source  Information about the address book being deleted.
+     *
+     * @return mixed  True on success or PEAR_Error on failure.
+     */
+    function deleteBook($source)
+    {
+        if (is_array($source)) {
+            // Not using shares
+            $params = $source['params'];
+            $bookName = $source['title'];
+        } else {
+            // Using shares.
+            $params = $GLOBALS['cfgSources']['imsp:' . $source]['params'];
+            $bookName = $source;
+        }
+        $imsp = &Net_IMSP::singleton('Book', $params);
+        $result = $imsp->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $imsp->deleteAddressBook($bookName);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+
+    /**
+     * Utility function to help clients create new address books without having
+     * to create an imsp driver instance first.
+     *
+     * @param array $source    Information about the user's default IMSP
+     *                         address book.
+     * @param string $newName  The name of the new address book.
+     *
+     * @return mixed  true on success or PEAR_Error on failure.
+     */
+    function createBook($source, $newName)
+    {
+        $imsp = &Net_IMSP::singleton('Book', $source['params']);
+        $result = $imsp->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // We now check if the username is already prepended to
+        // the address book name or not.
+        if (strpos($newName, $source['params']['username'] . '.') === 0) {
+            $name = $newName;
+        } else {
+            $name = $source['params']['username'] . '.' . $newName;
+        }
+        $result = $imsp->createAddressBook($name);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+
+    /**
+     * Synchronize Horde_Shares to existing IMSP address books.
+     *
+     * @param Horde_Share $share_obj  The Horde_Share object to use.
+     * @param array $serverInfo       Information about the IMSP server and
+     *                                the current user.
+     *
+     * @return mixed  Array describing any shares added or removed  | PEAR_Error.
+     * @since Horde 3.2
+     */
+    function synchShares(&$share_obj, $serverInfo)
+    {
+        $found_shares = array();
+        $return = array('added' => array(), 'removed' => array());
+        $params = array();
+
+        $imsp = &Net_IMSP::singleton('Book', $serverInfo['params']);
+        $result = $imsp->init();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $abooks = $imsp->getAddressBookList();
+        if (is_a($abooks, 'PEAR_Error')) {
+            return $abooks;
+        }
+
+        // Do we have a default address book? If not, create one.
+        if (array_search($serverInfo['params']['username'], $abooks) === false) {
+            $result = $imsp->createAddressbook($serverInfo['params']['username']);
+            if (!is_a($result, 'PEAR_Error')) {
+                // Make sure we add it to our list of books.
+                $abooks[] = $serverInfo['params']['username'];
+            }
+        }
+
+        $shares = &$share_obj->listShares(Horde_Auth::getAuth());
+        // A share for each IMSP adress book we can see.
+        foreach ($abooks as $abook_uid) {
+            $found = false;
+            foreach ($shares as $id => $share) {
+                $params = @unserialize($share->get('params'));
+                if (!empty($params['name']) && $params['name'] == $abook_uid &&
+                    $params['source'] == 'imsp') {
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                $shareparams = array('name' => $abook_uid,
+                                     'source' => 'imsp');
+
+                $params['uid'] = md5(mt_rand());
+                $params['name'] = $abook_uid . ' (IMSP)';
+                $params['acl'] = $imsp->myRights($abook_uid);
+                if ($abook_uid == $serverInfo['params']['username']) {
+                    // This is the default address book
+                    $shareparams['default'] = true;
+                } else {
+                    $shareparams['default'] = false;
+                }
+                if (Net_IMSP_Utils::_isOwner($abook_uid,
+                                             $serverInfo['params']['username'],
+                                             $params['acl'])) {
+                    $params['owner'] = Horde_Auth::getAuth();
+                } else {
+                    // TODO: What to do for the owner when it's not current user?
+                    //       We'd have to try to match the owner per IMSP
+                    //       address book name to a Horde user...how to do that
+                    //       without assuming usernames are equal?
+                }
+                $result = Net_IMSP_Utils::_createShare($share_obj, $params, $shareparams);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                $return['added'][] = $params['uid'];
+            } else {
+                // Share already exists, just update the acl.
+                $params['acl'] = $imsp->myRights($abook_uid);
+            }
+            $found_shares[] = $abook_uid;
+        }
+
+        // Now prune any shares that no longer exist on the IMSP server.
+        $existing = $share_obj->listShares(Horde_Auth::getAuth(), Horde_Perms::READ);
+        foreach ($existing as $key => $share) {
+            $temp = unserialize($share->get('params'));
+            if (is_array($temp)) {
+                $sourceType = $temp['source'];
+                if ($sourceType == 'imsp' &&
+                    array_search($temp['name'], $found_shares) === false) {
+                        $result = $share_obj->removeShare($share);
+                        if (is_a($result, 'PEAR_Error')) {
+                            return $result;
+                        }
+                        $return['removed'][] = $share->getName();
+                }
+            }
+        }
+        return $return;
+    }
+
+    /**
+     * Creates a Horde_Share for an *existing* IMSP address book.
+     * Needed for creating shares for address books created outside
+     * of Horde.
+     *
+     * @param Horde_Share  The share object to create the new share with.
+     * @param array        Parameters for the share
+     *
+     * @return mixed  True | PEAR_Error
+     * @since Horde 3.2
+     */
+    function _createShare(&$share_obj, $params, $shareparams)
+    {
+        $share = &$share_obj->newShare($params['uid']);
+        if (is_a($share, 'PEAR_Error')) {
+            return $share;
+        }
+        $share->set('params', serialize($shareparams));
+        $share->set('name', $params['name']);
+        Net_IMSP_Utils::_setPerms($share, $params['acl']);
+        $share->save();
+        return true;
+    }
+
+    /**
+     * Determine if we are the owner of the address book.
+     * Assumes ownership if username is beginning address book name or
+     * if user has admin rights ('a') in acl.
+     *
+     * @param array $params  Parameters to check for ownership.
+     *
+     * @return boolean  True if $user is owner, otherwise false.
+     * @since Horde 3.2
+     */
+    function _isOwner($bookName, $username, $acl)
+    {
+        if (strpos($bookName, $username) === 0) {
+            return true;
+        } elseif (strpos($acl, 'a')) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Translates IMSP acl into share permissions and sets them in share.
+     *
+     * @param Datatree_Object_Share $share  The share to assign perms to
+     * @param string $acl                   The IMSP acl string.
+     * @since Horde 3.2
+     */
+    function _setPerms(&$share, $acl)
+    {
+         $hPerms = 0;
+         if (strpos($acl, 'w') !== false) {
+             $hPerms |= Horde_Perms::EDIT;
+         }
+         if (strpos($acl, 'r') !== false) {
+             $hPerms |= Horde_Perms::READ;
+         }
+         if (strpos($acl, 'd') !== false) {
+             $hPerms |= Horde_Perms::DELETE;
+         }
+         if (strpos($acl, 'l') !== false) {
+             $hPerms |= Horde_Perms::SHOW;
+         }
+        $share->addUserPermission(Horde_Auth::getAuth(), $hPerms);
+    }
+
+    /**
+     * Translates Horde_Share permissions into IMSP acl.
+     *
+     * @param integer $perms   Horde_Perms style permission bitmask.
+     *
+     * @return string   An IMSP acl string
+     * @since Horde 3.2
+     */
+    function permsToACL($perms)
+    {
+        $acl = '';
+
+        if ($perms & Horde_Perms::SHOW) {
+            $acl = 'l';
+        }
+        if ($perms & Horde_Perms::READ) {
+            $acl .= 'r';
+        }
+        if ($perms & Horde_Perms::EDIT) {
+            $acl .= 'w';
+        }
+        if ($perms & Horde_Perms::DELETE) {
+            $acl .= 'd';
+        }
+        return $acl;
+    }
+
+    /**
+     * Set's an address book's acl on the IMSP server.
+     *
+     * @param string $book  The address book name to set
+     * @param string $name  The user name to set for.
+     * @param string $acl   The acl string to set.
+     *
+     * @return mixed  True | Pear_Error
+     * @since Horde 3.2
+     */
+    function setACL($params, $book, $name, $acl)
+    {
+        $imsp = &Net_IMSP::singleton('Book', $params);
+        $imsp->init();
+        return $imsp->setACL($book, $name, $acl);
+    }
+}
diff --git a/framework/Net_IMSP/package.xml b/framework/Net_IMSP/package.xml
new file mode 100644 (file)
index 0000000..8d133f7
--- /dev/null
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Net_IMSP</name>
+ <channel>pear.horde.org</channel>
+ <summary>IMSP API</summary>
+ <description>Provides an API into an IMSP server for Addressbooks and Options
+ </description>
+ <lead>
+  <name>Michael Rubinsky</name>
+  <user>mrubinsk</user>
+  <email>mrubinsk@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>22:36:36</time>
+ <version>
+  <release>0.0.6</release>
+  <api>0.0.6</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="IMSP">
+    <dir name="Auth">
+     <file baseinstalldir="/Net" name="cram_md5.php" role="php" />
+     <file baseinstalldir="/Net" name="imtest.php" role="php" />
+     <file baseinstalldir="/Net" name="plaintext.php" role="php" />
+    </dir> <!-- //IMSP/Auth -->
+    <file baseinstalldir="/Net" name="Auth.php" role="php" />
+    <file baseinstalldir="/Net" name="Book.php" role="php" />
+    <file baseinstalldir="/Net" name="Options.php" role="php" />
+    <file baseinstalldir="/Net" name="Utils.php" role="php" />
+   </dir> <!-- //IMSP -->
+   <file baseinstalldir="/Net" name="IMSP.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.5</release>
+    <api>0.0.5</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-01-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Suport for using Horde_Share added.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.4</release>
+    <api>0.0.4</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-04-12</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Support for automatically creating a user&apos;s default addressbook and
+functions added to the Net_IMSP_Utils class for easily adding and deleting
+addressbook.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-02-23</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Added support for authentication using mtest for Cyrus IMAP
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-02-06</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Bug fix for addressbook names containing spaces
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-02-05</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial packaging and release of code
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Net_SMS/SMS.php b/framework/Net_SMS/SMS.php
new file mode 100644 (file)
index 0000000..1cae7d6
--- /dev/null
@@ -0,0 +1,333 @@
+<?php
+
+require_once 'PEAR.php';
+
+/**
+ * Net_SMS Class
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS.php,v 1.26 2009/07/14 00:25:29 mrubinsk Exp $
+ *
+ * @author  Marko Djukic <marko@oblo.com>
+ * @package Net_SMS
+ */
+class Net_SMS {
+
+    /**
+     * A hash containing any parameters for the current gateway driver.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    var $_auth = null;
+
+    /**
+     * Constructor
+     *
+     * @param array $params  Any parameters needed for this gateway driver.
+     */
+    function Net_SMS($params = null)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Returns a list of available gateway drivers.
+     *
+     * @return array  An array of available drivers.
+     */
+    function getDrivers()
+    {
+        static $drivers = array();
+        if (!empty($drivers)) {
+            return $drivers;
+        }
+
+        $drivers = array();
+
+        if ($driver_dir = opendir(dirname(__FILE__) . '/SMS/')) {
+            while (false !== ($file = readdir($driver_dir))) {
+                /* Hide dot files and non .php files. */
+                if (substr($file, 0, 1) != '.' && substr($file, -4) == '.php') {
+                    $driver = substr($file, 0, -4);
+                    $driver_info = Net_SMS::getGatewayInfo($driver);
+                    $drivers[$driver] = $driver_info['name'];
+                }
+            }
+            closedir($driver_dir);
+        }
+
+        return $drivers;
+    }
+
+    /**
+     * Returns information on a gateway, such as name and a brief description,
+     * from the driver subclass getInfo() function.
+     *
+     * @return array  An array of extra information.
+     */
+    function getGatewayInfo($gateway)
+    {
+        static $info = array();
+        if (isset($info[$gateway])) {
+            return $info[$gateway];
+        }
+
+        require_once 'Net/SMS/' . $gateway . '.php';
+        $class = 'Net_SMS_' . $gateway;
+        $info[$gateway] = call_user_func(array($class, 'getInfo'));
+
+        return $info[$gateway];
+    }
+
+    /**
+     * Returns parameters for a gateway from the driver subclass getParams()
+     * function.
+     *
+     * @param string  The name of the gateway driver for which to return the
+     *                parameters.
+     *
+     * @return array  An array of extra information.
+     */
+    function getGatewayParams($gateway)
+    {
+        static $params = array();
+        if (isset($params[$gateway])) {
+            return $params[$gateway];
+        }
+
+        require_once 'Net/SMS/' . $gateway . '.php';
+        $class = 'Net_SMS_' . $gateway;
+        $params[$gateway] = call_user_func(array($class, 'getParams'));
+
+        return $params[$gateway];
+    }
+
+    /**
+     * Returns send parameters for a gateway from the driver subclass
+     * getDefaultSendParams()function. These are parameters which are available
+     * to the user during sending, such as setting a time for delivery, or type
+     * of SMS (normal text or flash), or source address, etc.
+     *
+     * @param string  The name of the gateway driver for which to return the
+     *                send parameters.
+     *
+     * @return array  An array of available send parameters.
+     */
+    function getDefaultSendParams($gateway)
+    {
+        static $params = array();
+        if (isset($params[$gateway])) {
+            return $params[$gateway];
+        }
+
+        require_once 'Net/SMS/' . $gateway . '.php';
+        $class = 'Net_SMS_' . $gateway;
+        $params[$gateway] = call_user_func(array($class, 'getDefaultSendParams'));
+
+        return $params[$gateway];
+    }
+
+    /**
+     * Query the current Gateway object to find out if it supports the given
+     * capability.
+     *
+     * @param string $capability  The capability to test for.
+     *
+     * @return mixed  Whether or not the capability is supported or any other
+     *                value that the capability wishes to report.
+     */
+    function hasCapability($capability)
+    {
+        if (!empty($this->capabilities[$capability])) {
+            return $this->capabilities[$capability];
+        }
+        return false;
+    }
+
+    /**
+     * Authenticates against the gateway if required.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function authenticate()
+    {
+        /* Do authentication for this gateway if driver requires it. */
+        if ($this->hasCapability('auth')) {
+            $this->_auth = $this->_authenticate();
+            return $this->_auth;
+        }
+        return true;
+    }
+
+    /**
+     * Sends a message to one or more recipients. Hands off the actual sending
+     * to the gateway driver.
+     *
+     * @param array $message  The message to be sent, which is composed of:
+     *                        <pre>
+     *                          id   - A unique ID for the message;
+     *                          to   - An array of recipients;
+     *                          text - The text of the message;
+     *                        </pre>
+     *
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function send($message)
+    {
+        /* Authenticate. */
+        if (is_a($this->authenticate(), 'PEAR_Error')) {
+            return $this->_auth;
+        }
+
+        /* Make sure the recipients are in an array. */
+        if (!is_array($message['to'])) {
+            $message['to'] = array($message['to']);
+        }
+
+        /* Array to store each send. */
+        $sends = array();
+
+        /* If gateway supports batch sending, preference is given to this
+         * method. */
+        if ($max_per_batch = $this->hasCapability('batch')) {
+            /* Split up the recipients in the max recipients per batch as
+             * supported by gateway. */
+            $iMax = count($message['to']);
+            $batches = ceil($iMax / $max_per_batch);
+
+            /* Loop through the batches and compose messages to be sent. */
+            for ($b = 0; $b < $batches; $b++) {
+                $recipients = array_slice($message['to'], ($b * $max_per_batch), $max_per_batch);
+                $response = $this->_send($message, $recipients);
+                foreach ($recipients as $recipient) {
+                    if ($response[$recipient][0] == 1) {
+                        /* Message was sent, store remote id. */
+                        $remote_id = $response[$recipient][1];
+                        $error = null;
+                    } else {
+                        /* Message failed, store error code. */
+                        $remote_id = null;
+                        $error = $response[$recipient][1];
+                    }
+
+                    /* Store the sends. */
+                    $sends[] = array('message_id' => $message['id'],
+                                     'remote_id'  => $remote_id,
+                                     'recipient'  => $recipient,
+                                     'error'      => $error);
+                }
+            }
+        } else {
+            /* No batch sending available, just loop through all recipients
+             * and send a message for each one. */
+            foreach ($message['to'] as $recipient) {
+                $response = $this->_send($message, $recipient);
+                if ($response[0] == 1) {
+                    /* Message was sent, store remote id if any. */
+                    $remote_id = (isset($response[1]) ? $response[1] : null);
+                    $error = null;
+                } else {
+                    /* Message failed, store error code. */
+                    $remote_id = null;
+                    $error = $response[1];
+                }
+
+                /* Store the sends. */
+                $sends[] = array('message_id' => $message['id'],
+                                 'remote_id'  => $remote_id,
+                                 'recipient'  => $recipient,
+                                 'error'      => $error);
+            }
+        }
+
+        return $sends;
+    }
+
+    /**
+     * If the current driver has a credit capability, queries the gateway for
+     * a credit balance and returns the value.
+     *
+     * @return integer  Value indicating available credit or null if not
+     *                  supported.
+     */
+    function getBalance()
+    {
+        /* Authenticate. */
+        if (is_a($this->authenticate(), 'PEAR_Error')) {
+            return $this->_auth;
+        }
+
+        /* Check balance. */
+        if ($this->hasCapability('credit')) {
+            return $this->_getBalance();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Attempts to return a concrete Gateway instance based on $driver.
+     *
+     * @param string $driver  The type of concrete Gateway subclass to return.
+     *                        This is based on the gateway driver ($driver).
+     *                        The code is dynamically included.
+     * @param array $params   A hash containing any additional configuration or
+     *                        connection parameters a subclass might need.
+     *
+     * @return Net_SMS  The newly created concrete Gateway instance or false on
+     *                  an error.
+     */
+    function &factory($driver, $params = array())
+    {
+        include_once 'Net/SMS/' . $driver . '.php';
+        $class = 'Net_SMS_' . $driver;
+        if (class_exists($class)) {
+            $sms = new $class($params);
+        } else {
+            $sms = PEAR::raiseError(sprintf(_("Class definition of %s not found."), $driver));
+        }
+
+        return $sms;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete Net_SMS instance based on
+     * $driver.
+     *
+     * It will only create a new instance if no Net_SMS instance with the same
+     * parameters currently exists.
+     *
+     * This method must be invoked as: $var = &Net_SMS::singleton()
+     *
+     * @param string $driver  The type of concrete Net_SMS subclass to return.
+     *                        The is based on the gateway driver ($driver).
+     *                        The code is dynamically included.
+     *
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return mixed  The created concrete Net_SMS instance, or false on error.
+     */
+    function &singleton($driver, $params = array())
+    {
+        static $instances;
+        if (!isset($instances)) {
+            $instances = array();
+        }
+
+        $signature = serialize(array($driver, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &Net_SMS::factory($driver, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/clickatell_http.php b/framework/Net_SMS/SMS/clickatell_http.php
new file mode 100644 (file)
index 0000000..cee55d3
--- /dev/null
@@ -0,0 +1,356 @@
+<?php
+/**
+ * @package Net_SMS
+ */
+
+/**
+ * HTTP_Request class.
+ */
+include_once 'HTTP/Request.php';
+
+/**
+ * Net_SMS_clickatell_http Class implements the HTTP API for accessing the
+ * Clickatell (www.clickatell.com) SMS gateway.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS/clickatell_http.php,v 1.30 2009/01/06 17:49:34 jan Exp $
+ *
+ * @author Marko Djukic <marko@oblo.com>
+ * @package Net_SMS
+ */
+class Net_SMS_clickatell_http extends Net_SMS {
+
+    var $_session_id = null;
+    var $_base_url = 'http://api.clickatell.com/http/';
+
+    /**
+     * An array of capabilities, so that the driver can report which operations
+     * it supports and which it doesn't. Possible values are:<pre>
+     *   auth        - The gateway require authentication before sending;
+     *   batch       - Batch sending is supported;
+     *   multi       - Sending of messages to multiple recipients is supported;
+     *   receive     - Whether this driver is capable of receiving SMS;
+     *   credit      - Is use of the gateway based on credits;
+     *   addressbook - Are gateway addressbooks supported;
+     *   lists       - Gateway support for distribution lists.
+     * </pre>
+     *
+     * @var array
+     */
+    var $capabilities = array('auth'        => true,
+                              'batch'       => 100,
+                              'multi'       => true,
+                              'receive'     => false,
+                              'credit'      => true,
+                              'addressbook' => false,
+                              'lists'       => false);
+
+    /**
+     * Authenticate at the gateway and set a session id if successful. Caching
+     * is used to minimise the http calls for subsequent messages.
+     *
+     * @access private
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function _authenticate()
+    {
+        /* We have already authenticated so return true. */
+        if (!empty($this->_session_id)) {
+            return true;
+        }
+
+        /* Set up the http authentication url. */
+        $url = sprintf('auth?user=%s&password=%s&api_id=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $this->_params['api_id']);
+
+        /* Do the HTTP authentication and get the response. */
+        $response = Net_SMS_clickatell_http::_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Authentication failed. %s"), $response->getMessage()));
+        }
+
+        /* Split up the response. */
+        $response = explode(':', $response);
+        if ($response[0] == 'OK') {
+            $this->_session_id = trim($response[1]);
+            return true;
+        } else {
+            return $this->getError($response[1], _("Authentication failed. %s"));
+        }
+    }
+
+    /**
+     * This function does the actual sending of the message.
+     *
+     * @access private
+     *
+     * @param array $message  The array containing the message and its send
+     *                        parameters.
+     * @param array $to       The recipients.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        /* Set up the http sending url. */
+        $url = sprintf('sendmsg?session_id=%s&text=%s',
+                       $this->_session_id,
+                       urlencode($message['text']));
+
+        $req_feat = 0;
+        if (!empty($message['send_params']['from'])) {
+            /* If source from is set, require it for transit gateways and append
+               to url. */
+            $req_feat =+ 16;
+            $url .= '&from=' . urlencode($message['send_params']['from']);
+        }
+        if (!empty($message['send_params']['msg_type']) &&
+            $message['send_params']['msg_type'] == 'SMS_FLASH') {
+            /* If message type is flash, require it for transit gateways. */
+            $req_feat =+ 512;
+            $url .= '&msg_type=' . $message['send_params']['msg_type'];
+        }
+        if (!empty($req_feat)) {
+            /* If features have been required, add to url. */
+            $url .= '&req_feat=' . $req_feat;
+        }
+
+        /* Append the recipients of this message and call the url. */
+        foreach ($to as $key => $val) {
+            if (preg_match('/^.*?<?\+?(\d{7,})(>|$)/', $val, $matches)) {
+                $to[$key] = $matches[1];
+            } else {
+                /* FIXME: Silently drop bad recipients. This should be logged
+                 * and/or reported. */
+                unset($to[$key]);
+            }
+        }
+        $to = implode(',', $to);
+        $url .= '&to=' . $to;
+        $response = trim($this->_callURL($url));
+
+        /* Ugly parsing of the response, but that's how it comes back. */
+        $lines = explode("\n", $response);
+        $response = array();
+
+        if (count($lines) > 1) {
+            foreach ($lines as $line) {
+                $parts = explode('To:', $line);
+                $recipient = trim($parts[1]);
+                $outcome = explode(':', $parts[0]);
+                $response[$recipient] = array(($outcome[0] == 'ID' ? 1 : 0), $outcome[1]);
+            }
+        } else {
+            /* Single recipient. */
+            $outcome = explode(':', $lines[0]);
+            $response[$to] = array(($outcome[0] == 'ID' ? 1 : 0), $outcome[1]);
+        }
+
+        return $response;
+    }
+
+    /**
+     * Returns the current credit balance on the gateway.
+     *
+     * @access private
+     *
+     * @return integer  The credit balance available on the gateway.
+     */
+    function _getBalance()
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('getbalance?session_id=%s',
+                       $this->_session_id);
+        $response = trim($this->_callURL($url));
+
+        /* Try splitting up the response. */
+        $lines = explode('=', $response);
+
+        /* Split up the response. */
+        $response = explode(':', $response);
+        if ($response[0] == 'Credit') {
+            return trim($response[1]);
+        } else {
+            return $this->getError($response[1], _("Could not check balance. %s"));
+        }
+    }
+
+    /**
+     * Identifies this gateway driver and returns a brief description.
+     *
+     * @return array  Array of driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("Clickatell via HTTP"),
+            'desc' => _("This driver allows sending of messages through the Clickatell (http://clickatell.com) gateway, using the HTTP API"),
+        );
+    }
+
+    /**
+     * Returns the required parameters for this gateway driver.
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        return array(
+            'user' => array('label' => _("Username"), 'type' => 'text'),
+            'password' => array('label' => _("Password"), 'type' => 'text'),
+            'api_id' => array('label' => _("API ID"), 'type' => 'text'),
+        );
+    }
+
+    /**
+     * Returns the parameters that can be set as default for sending messages
+     * using this gateway driver and displayed when sending messages.
+     *
+     * @return array  Array of parameters that can be set as default.
+     * @todo  Set up batch fields/params, would be nice to have ringtone/logo
+     *        support too, queue choice, unicode choice.
+     */
+    function getDefaultSendParams()
+    {
+        $params = array();
+        $params['from'] = array(
+            'label' => _("Source address"),
+            'type' => 'text');
+
+        $params['deliv_time'] = array(
+            'label' => _("Delivery time"),
+            'type' => 'enum',
+            'params' => array(array('now' => _("immediate"), 'user' => _("user select"))));
+
+        $types = array('SMS_TEXT' => _("Standard"), 'SMS_FLASH' => _("Flash"));
+        $params['msg_type'] = array(
+            'label' => _("Message type"),
+            'type' => 'keyval_multienum',
+            'params' => array($types));
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters for sending messages using this gateway driver,
+     * displayed when sending messages. These are filtered out using the
+     * default values set for the gateway.
+     *
+     * @return array  Array of required parameters.
+     * @todo  Would be nice to use a time/date setup rather than minutes from
+     *        now for the delivery time. Upload field for ringtones/logos?
+     */
+    function getSendParams($params)
+    {
+        if (empty($params['from'])) {
+            $params['from'] = array(
+                'label' => _("Source address"),
+                'type' => 'text');
+        }
+
+        if ($params['deliv_time'] == 'user') {
+            $params['deliv_time'] = array(
+                'label' => _("Delivery time"),
+                'type' => 'int',
+                'desc' => _("Value in minutes from now."));
+        }
+
+        if (count($params['msg_type']) > 1) {
+            $params['msg_type'] = array(
+                'label' => _("Message type"),
+                'type' => 'enum',
+                'params' => array($params['msg_type']));
+        } else {
+            $params['msg_type'] = $params['msg_type'][0];
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns a string representation of an error code.
+     *
+     * @param integer $error  The error code to look up.
+     * @param string $text    An existing error text to use to raise a
+     *                        PEAR Error.
+     *
+     * @return mixed  A textual message corresponding to the error code or a
+     *                PEAR Error if passed an existing error text.
+     *
+     * @todo  Check which of these are actually required and trim down the
+     *        list.
+     */
+    function getError($error, $error_text = '')
+    {
+        /* Make sure we get only the number at the start of an error. */
+        list($error) = explode(',', $error);
+        $error = trim($error);
+
+        /* An array of error codes returned by the gateway. */
+        $errors = array('001' => _("Authentication failed"),
+                        '002' => _("Unknown username or password."),
+                        '003' => _("Session ID expired."),
+                        '004' => _("Account frozen."),
+                        '005' => _("Missing session ID."),
+                        '007' => _("IP lockdown violation."),
+                        '101' => _("Invalid or missing parameters."),
+                        '102' => _("Invalid UDH. (User Data Header)."),
+                        '103' => _("Unknown apimsgid (API Message ID)."),
+                        '104' => _("Unknown climsgid (Client Message ID)."),
+                        '105' => _("Invalid destination address."),
+                        '106' => _("Invalid source address."),
+                        '107' => _("Empty message."),
+                        '108' => _("Invalid or missing api_id."),
+                        '109' => _("Missing message ID."),
+                        '110' => _("Error with email message."),
+                        '111' => _("Invalid protocol."),
+                        '112' => _("Invalid msg_type."),
+                        '113' => _("Max message parts exceeded."),
+                        '114' => _("Cannot route message to specified number."),
+                        '115' => _("Message expired."),
+                        '116' => _("Invalid unicode data."),
+                        '201' => _("Invalid batch ID."),
+                        '202' => _("No batch template."),
+                        '301' => _("No credit left."),
+                        '302' => _("Max allowed credit."));
+
+        if (empty($error_text)) {
+            return $errors[$error];
+        } else {
+            return PEAR::raiseError(sprintf($error_text, $errors[$error]));
+        }
+    }
+
+    /**
+     * Do the http call using a url passed to the function.
+     *
+     * @access private
+     *
+     * @param string $url  The url to call.
+     *
+     * @return mixed  The response on success or PEAR Error on failure.
+     */
+    function _callURL($url)
+    {
+        $options['method'] = 'GET';
+        $options['timeout'] = 5;
+        $options['allowRedirects'] = true;
+
+        $http = new HTTP_Request($this->_base_url . $url, $options);
+        @$http->sendRequest();
+        if ($http->getResponseCode() != 200) {
+            return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
+        }
+
+        return $http->getResponseBody();
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/generic_smpp.php b/framework/Net_SMS/SMS/generic_smpp.php
new file mode 100644 (file)
index 0000000..bdfcf36
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+
+include_once 'Net/SMPP/Client.php';
+
+/**
+ * SMPP based SMS driver.
+ *
+ * This driver interfaces with the email-to-sms gateways provided by many
+ * carriers, particularly those based in the U.S.
+ *
+ * $Horde: framework/Net_SMS/SMS/generic_smpp.php,v 1.2 2007/06/27 17:22:58 jan Exp $
+ *
+ * Copyright 2005-2007 WebSprockets, LLC
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category   Networking
+ * @package    Net_SMS
+ * @author     Ian Eure <ieure@php.net>
+ * @link       http://pear.php.net/package/Net_SMS
+ * @since      Net_SMS 0.2.0
+ * @since      Horde 3.2
+ */
+class Net_SMS_generic_smpp extends Net_SMS {
+
+    /**
+     * Capabilities of this driver
+     *
+     * @var  array
+     */
+    var $capabilities = array(
+        'auth'        => true,
+        'batch'       => false,
+        'multi'       => false,
+        'receive'     => false,
+        'credit'      => false,
+        'addressbook' => false,
+        'lists'       => false
+    );
+
+    /**
+     * Driver parameters
+     *
+     * @var     array
+     * @access  private
+     */
+    var $_params = array(
+        'host'         => null,
+        'port'         => 0,
+        'vendor'       => null,
+        'bindParams'   => array(),
+        'submitParams' => array()
+    );
+
+    /**
+     * Net_SMPP_Client instance
+     *
+     * @var     Net_SMPP_Client
+     * @access  private
+     */
+    var $_client = null;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  Parameters.
+     */
+    function Net_SMS_generic_smpp($params = null)
+    {
+        parent::Net_SMS($params);
+        $this->_client =& new Net_SMPP_Client($this->_params['host'], $this->_params['port']);
+        if (!is_null($this->_params['vendor'])) {
+            Net_SMPP::setVendor($this->_params['vendor']);
+        }
+    }
+
+    /**
+     * Identifies this driver.
+     *
+     * @return array  Driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("SMPP Gateway"),
+            'desc' => _("This driver allows sending of messages through an SMPP gateway.")
+        );
+    }
+
+    /**
+     * Get required paramaters
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        return array(
+            'host' => array(
+                'label' => _("Host"), 'type' => 'text'),
+            'port' => array(
+                'label' => _("Port"), 'type' => 'int'),
+//             'bindParams' => array(
+//                 'label' => _('bind_transmitter paramaters'), 'type' => 'array'),
+//             'submitParams' => array(
+//                 'label' => _('submit_sm parameters'), 'type' => 'array'
+//             )
+        );
+    }
+
+    /**
+     * Sends the message.
+     *
+     * @access  private
+     *
+     * @param array $message  Message to send.
+     * @param string $to      The recipient.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        $pdu =& Net_SMPP::PDU('submit_sm', $this->_params['submitParams']);
+        $pdu->destination_addr = $to;
+        $pdu->short_message = $message['text'];
+        if (count($message) > 1) {
+            // Other params to set
+            $v = $message;
+            unset($v['text']);
+            $pdu->set($v);
+            unset($v);
+        }
+
+        $res =& $this->_client->sendPDU($pdu);
+
+        // Error sending?
+        if ($res === false) {
+            return array(0, _("Error sending PDU"));
+        }
+
+        $resp =& $this->_client->readPDU();
+        if ($resp === false) {
+            return array(0, _("Could not read response PDU"));
+        }
+        if ($resp->isError()) {
+            return array(0, sprintf(_("Sending failed: %s") . $resp->statusDesc()));
+        }
+
+        // Success!
+        return array(1, $resp->message_id);
+    }
+
+    /**
+     * Authenticates with the SMSC.
+     *
+     * This method connects to the SMSC (if not already connected) and
+     * authenticates with the bind_transmitter command (if not already bound).
+     *
+     * @access  protected
+     */
+    function _authenticate()
+    {
+        if ($this->_client->state == NET_SMPP_CLIENT_STATE_CLOSED) {
+            $res = $this->_client->connect();
+            if ($res === false) {
+                return false;
+            }
+        }
+
+        if ($this->_client->state == NET_SMPP_CLIENT_STATE_OPEN) {
+            $resp =& $this->_client->bind($this->_params['bindParams']);
+            if ($resp === false || (is_object($resp) && $resp->isError())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Accepts an object.
+     *
+     * @see Net_SMPP_Client::accept()
+     *
+     * @return mixed  {@link Net_SMPP_Client::accept()}'s return value
+     */
+    function accept(&$obj)
+    {
+        return $this->_client->accept($obj);
+    }
+
+    /**
+     * Returns a list of parameters specific for this driver.
+     *
+     * @return array Default sending parameters.
+     */
+    function getDefaultSendParams()
+    {
+        return array();
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Net_SMS/SMS/generic_smtp.php b/framework/Net_SMS/SMS/generic_smtp.php
new file mode 100644 (file)
index 0000000..1d6080d
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Generic e-mail based SMS driver
+ *
+ * Copyright 2005-2007 WebSprockets, LLC
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * This driver interfaces with the email-to-sms gateways provided by many
+ * carriers, particularly those based in the U.S.
+ *
+ * $Horde: framework/Net_SMS/SMS/generic_smtp.php,v 1.16 2008/05/14 21:29:23 jan Exp $
+ *
+ * @category   Networking
+ * @package    Net_SMS
+ * @author     Ian Eure <ieure@php.net>
+ * @since      Horde 3.1
+ * @since      Net_SMS 0.0.2
+ */
+class Net_SMS_generic_smtp extends Net_SMS {
+
+    /**
+     * Capabilities of this driver.
+     *
+     * @var array
+     */
+    var $capabilities = array(
+        'auth'        => false,
+        'batch'       => false,
+        'multi'       => false,
+        'receive'     => false,
+        'credit'      => false,
+        'addressbook' => false,
+        'lists'       => false
+    );
+
+    /**
+     * Driver parameters.
+     *
+     * @var array
+     *
+     * @access private
+     */
+    var $_params = array(
+        'carrier'     => null,
+        'mailBackend' => 'mail',
+        'mailParams'  => array(),
+        'mailHeaders' => array()
+    );
+
+    /**
+     * Carrier email map.
+     *
+     * @var array
+     *
+     * @access private
+     */
+    var $_carriers = array(
+        /* U.S. carriers. */
+        'att'          => '%s@mmode.com',
+        'cingular'     => '%s@mmode.com',
+        'verizon'      => '%s@vtext.com',
+        'boost'        => '%s@myboostmobile.com',
+        'cellularone'  => '%s@mycellone.net',
+        'cincybell'    => '%s@gocbw.com',
+        'sprint'       => '%s@messaging.sprintpcs.com',
+        'tmobile_us'   => '%s@tmomail.com',
+        'suncom'       => '%s@tms.suncom.com',
+        'aircel'       => '%s@airsms.com',
+        'airtel'       => '%s@airtelmail.com',
+        'bplmobile'    => '%s@bplmobile.com',
+        'bellmobility' => '%s@txt.bellmobility.ca',
+        'bluegrass'    => '%s@sms.bluecell.com',
+        'cellforce'    => '%s@celforce.com',
+        'cellularone'  => '%s@mycellone.net',
+        /* German carriers. */
+        'eplus'       => '%s@smsmail.eplus.de',
+        'tmobile_de'  => '%s@t-mobile-sms.de',
+        'vodafone_de' => '%s@vodafone-sms.de',
+    );
+
+    /**
+     * Identifies this driver.
+     *
+     * @return array  Driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("Email-to-SMS Gateway"),
+            'desc' => _("This driver allows sending of messages through an email-to-SMS gateway, for carriers which provide this service.")
+        );
+    }
+
+    /**
+     * Returns required parameters.
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        return array(
+            'carrier'     => array('label' => _("Carrier"), 'type' => 'text'),
+            'mailBackend' => array('label' => _("PEAR::Mail backend"), 'type' => 'text')
+        );
+    }
+
+    /**
+     * Sends the message.
+     *
+     * You may also specify the carrier with the 'carrier' key of the message
+     * to avoid creating a new instance for each carrier, or fiddling with the
+     * parameters.
+     *
+     * @access private
+     *
+     * @param array $message  Message to send.
+     * @param string $to      The recipient.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        require_once 'Mail.php';
+        $m = &Mail::factory($this->_params['mailBackend'],
+                            $this->_params['mailParams']);
+
+        if (isset($message['carrier'])) {
+            $dest = $this->_getDest($to, $message['carrier']);
+        } else {
+            $dest = $this->_getDest($to);
+        }
+
+        $res = $m->send($dest, $this->_params['mailHeaders'], $message['text']);
+        if (PEAR::isError($res)) {
+            return array(0, $res->getMessage());
+        } else {
+            return array(1, null);
+        }
+    }
+
+    /**
+     * Returns destination e-mail address.
+     *
+     * @param string $phone  Phone number to send to.
+     *
+     * @return string  Destination address.
+     */
+    function _getDest($phone, $carrier = null)
+    {
+        $carrier = is_null($carrier) ? $this->_params['carrier'] : $carrier;
+        return sprintf($this->_carriers[$carrier],
+                       preg_replace('/[^0-9]/', '', $phone));
+    }
+
+    /**
+     * Returns the address template for a carrier.
+     *
+     * @param string $carrier  Carrier name.
+     *
+     * @return mixed  Address template or false.
+     */
+    function getAddressTemplate($carrier)
+    {
+        if (!isset($this->_carriers[$carrier])) {
+            return false;
+        }
+        return $this->_carriers[$carrier];
+    }
+
+    /**
+     * Adds a carrier to the list.
+     *
+     * Address templates need to be in the form of an email address, with a
+     * '%s' representing the place where the destination phone number goes.
+     *
+     * @param string $name  Carrier name.
+     * @param string $addr  Address template.
+     */
+    function addCarrier($name, $addr)
+    {
+        $this->_carriers[$name] = $addr;
+    }
+
+    /**
+     * Returns a list of parameters specific for this driver.
+     *
+     * @return array Default sending parameters.
+     */
+    function getDefaultSendParams()
+    {
+        return array();
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/sms2email_http.php b/framework/Net_SMS/SMS/sms2email_http.php
new file mode 100644 (file)
index 0000000..62ec549
--- /dev/null
@@ -0,0 +1,689 @@
+<?php
+/**
+ * @package Net_SMS
+ */
+
+/**
+ * HTTP_Request class.
+ */
+include_once 'HTTP/Request.php';
+
+/**
+ * Net_SMS_sms2email_http Class implements the HTTP API for accessing the
+ * sms2email (www.sms2email.com) SMS gateway.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS/sms2email_http.php,v 1.42 2009/01/06 17:49:34 jan Exp $
+ *
+ * @author Marko Djukic <marko@oblo.com>
+ * @package Net_SMS
+ */
+class Net_SMS_sms2email_http extends Net_SMS {
+
+    var $_base_url = 'horde.sms2email.com/horde/';
+
+    /**
+     * An array of capabilities, so that the driver can report which
+     * operations it supports and which it doesn't. Possible values are:<pre>
+     *   auth        - The gateway requires authentication before sending;
+     *   batch       - Batch sending is supported;
+     *   multi       - Sending of messages to multiple recipients is supported;
+     *   receive     - Whether this driver is capable of receiving SMS;
+     *   credit      - Is use of the gateway based on credits;
+     *   addressbook - Are gateway addressbooks supported;
+     *   lists       - Gateway support for distribution lists.
+     * </pre>
+     *
+     * @var array
+     */
+    var $capabilities = array('auth'        => false,
+                              'batch'       => 100,
+                              'multi'       => true,
+                              'receive'     => false,
+                              'credit'      => true,
+                              'addressbook' => true,
+                              'lists'       => true);
+
+    /**
+     * This function does the actual sending of the message.
+     *
+     * @access private
+     *
+     * @param array $message  The array containing the message and its send
+     *                        parameters.
+     * @param array $to       The recipients.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        /* Set up the sending url. */
+        $url = sprintf('postmsg.php?username=%s&password=%s&message=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       urlencode($message['text']));
+
+        /* Check if source from is set. */
+        if (!empty($message['send_params']['from'])) {
+            $url .= '&orig=' . urlencode($message['send_params']['from']);
+        }
+        /* Check if message type is flash. */
+        if (!empty($message['send_params']['msg_type']) &&
+            $message['send_params']['msg_type'] == 'SMS_FLASH') {
+            $url .= '&flash=1';
+        }
+        /* Check if delivery report url has been set. */
+        if (!empty($this->_params['delivery_report'])) {
+            $url .= '&dlurl=' . urlencode($this->_params['delivery_report']) .
+                    'reportcode=%code&destinationnumber=%dest';
+        }
+
+        /* Loop through recipients and do some minimal validity checking. */
+        if (is_array($to)) {
+            foreach ($to as $key => $val) {
+                if (preg_match('/^.*?<?\+?(\d{7,})(>|$)/', $val, $matches)) {
+                    $to[$key] = $matches[1];
+                } else {
+                    /* FIXME: Silently drop bad recipients. This should be
+                     * logged and/or reported. */
+                    unset($to[$key]);
+                }
+            }
+            $to = implode(',', $to);
+        } else {
+            if (preg_match('/^.*?<?\+?(\d{7,})(>|$)/', $to, $matches)) {
+                $to = $matches[1];
+            } else {
+                return array(0, sprintf(_("Invalid recipient: \"%s\""), $to));
+            }
+        }
+
+        /* Append the recipients of this message and call the url. */
+        $url .= '&to_num=' . $to;
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return array(0, $response->getMessage());
+        }
+
+        /* Parse the response, check for new lines in case of multiple
+         * recipients. */
+        $lines = explode("\n", $response);
+        $response = array();
+
+        if (count($lines) > 1) {
+            /* Multiple recipients. */
+            foreach ($lines as $line) {
+                $parts = explode('To:', $line);
+                $recipient = trim($parts[1]);
+                if ($lines[0] == 'AQSMS-OK') {
+                    $response[$recipient] = array(1, null);
+                } else {
+                    $response[$recipient] = array(0, $lines[0]);
+                }
+            }
+        } else {
+            /* Single recipient. */
+            if ($lines[0] == 'AQSMS-OK') {
+                $response[$to] = array(1, null);
+            } else {
+                $response[$to] = array(0, $lines[0]);
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * Returns the current credit balance on the gateway.
+     *
+     * @access private
+     *
+     * @return integer  The credit balance available on the gateway.
+     */
+    function _getBalance()
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postmsg.php?username=%s&password=%s&cmd=credit',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']));
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Try splitting up the response. */
+        $lines = explode('=', $response);
+
+        if ($lines[0] == 'AQSMS-CREDIT') {
+            return $lines[1];
+        } else {
+            return $this->getError($lines[0], _("Could not check balance. %s"));
+        }
+    }
+
+    /**
+     * Adds a contact to the gateway's addressbook.
+     *
+     * @param string $name     The name for this contact
+     * @param integer $number  The contact's phone number.
+     *
+     * @return mixed  The remote contact ID on success or PEAR Error on
+     *                failure.
+     */
+    function addContact($name, $number)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postcontacts.php?username=%s&password=%s&cmd=ADDCONTACT&name=%s&number=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       urlencode($name),
+                       $number);
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check if there was an error response. */
+        if (substr($response, 0, 17) != 'AQSMS-CONTACTIDOK') {
+            return $this->getError($response, _("Could not add contact. %s"));
+        }
+
+        /* Split up the response. */
+        $lines = explode(',=', $response);
+        return $lines[1];
+    }
+
+    /**
+     * Updates a contact in the gateway's addressbook.
+     *
+     * @param integer $id      The contact's ID on the gateway.
+     * @param string $name     The name for this contact
+     * @param integer $number  The contact's phone number.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function updateContact($id, $name, $number)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postcontacts.php?username=%s&password=%s&cmd=UPDATECONTACT&id=%s&name=%s&number=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $id,
+                       urlencode($name),
+                       $number);
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Parse the response. */
+        if ($response == 'AQSMS-OK') {
+            return true;
+        } else {
+            return $this->getError($response, _("Could not update contact. %s"));
+        }
+    }
+
+    /**
+     * Deletes a contact in the gateway's addressbook.
+     *
+     * @param integer $id  The contact's ID on the gateway.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function deleteContact($id)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postcontacts.php?username=%s&password=%s&cmd=DELETECONTACT&id=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $id);
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Parse the response. */
+        if ($response == 'AQSMS-OK') {
+            return true;
+        } else {
+            return $this->getError($response, _("Could not delete contact. %s"));
+        }
+    }
+
+    /**
+     * Fetches the entire address book from the gateway.
+     *
+     * @return mixed  Array of contacts on success or PEAR Error on failure.
+     *                Format of the returned contacts is for example:<code>
+     *                   array(<uniqueid> => array('name'   => <name>,
+     *                                             'number' => <number>),
+     *                         <uniqueid> => array('name'   => <name>,
+     *                                             'number' => <number>));
+     * </code>
+     */
+    function getAddressBook()
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postcontacts.php?username=%s&password=%s&cmd=GETADDRESSBOOK',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']));
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check if there was an error response. */
+        if (substr($response, 0, 19) != 'AQSMS-ADDRESSBOOKOK') {
+            return $this->getError($response, _("Could not retrieve address book. %s"));
+        }
+
+        /* Parse the response and construct the array. */
+        list($response, $contacts_str) = explode(',', $response, 2);
+
+        /* Check that the full address book list has been received. */
+        $length = substr($response, 19);
+        if (strlen($contacts_str) != $length) {
+            return PEAR::raiseError(_("Could not fetch complete address book."));
+        }
+        $contacts_lines = explode("\n", $contacts_str);
+        $contacts = array();
+        /* Loop through lines and pick out the fields, make sure that the ""
+         * are not included in the values, so get the line less 1 char on each
+         * end and split for ",". */
+        foreach ($contacts_lines as $line) {
+            list($id, $name, $number) = explode('","', substr($line, 1, -1));
+            $contacts[$id] = array('name' => $name, 'number' => $number);
+        }
+
+        return $contacts;
+    }
+
+    /**
+     * Creates a new distribution list on the gateway.
+     *
+     * @param string $name    An arbitrary name for the new list.
+     * @param array $numbers  A simple array of numbers to add to the list.
+     *
+     * @return mixed  Gateway ID for the created list on success or PEAR Error
+     *                on failure.
+     */
+    function listCreate($name, $numbers)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postdistribution.php?username=%s&password=%s&cmd=ADDDISTLIST&name=%s&numlist=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       urlencode($name),
+                       implode(',', $numbers));
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check if there was an error response. */
+        if (substr($response, 0, 16) != 'AQSMS-DISTITEMID') {
+            return $this->getError($response, _("Could not create distribution list. %s"));
+        }
+
+        /* Parse the response and get the distribution list ID. */
+        list($response, $id) = explode('=', $response);
+
+        /* TODO: do we need to check the length of the id string? */
+
+        return $id;
+    }
+
+    /**
+     * Deletes a distribution list from the gateway.
+     *
+     * @param string $id  The gateway ID for the list to delete.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function listDelete($id)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postdistribution.php?username=%s&password=%s&cmd=DELETEDISTLIST&distid=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $id);
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check response. */
+        if ($response == 'AQSMS-OK') {
+            return true;
+        } else {
+            return $this->getError($response, _("Could not delete distribution list. %s"));
+        }
+    }
+
+    /**
+     * Updates a distribution list on the gateway.
+     *
+     * @param string $id       The gateway ID for the list to update.
+     * @param string $name     The arbitrary name of the list. If different
+     *                         from the original name that the list was created
+     *                         under, the list will be renamed.
+     * @param string $numbers  The new list of numbers in the list. If left
+     *                         empty, the result will be the same as calling
+     *                         the listRename() function.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function listUpdate($id, $name, $numbers = array())
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postdistribution.php?username=%s&password=%s&cmd=UPDATELISTNAME&distid=%s&name=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $id,
+                       urlencode($name));
+
+        /* Check if the list numbers need updating. */
+        if (!empty($numbers)) {
+            $url .= '&numbers=' . implode(',', $numbers);
+        }
+
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check response. */
+        if ($response == 'AQSMS-OK') {
+            return true;
+        } else {
+            return $this->getError($response, _("Could not update distribution list. %s"));
+        }
+    }
+
+    /**
+     * Renames a distribution list on the gateway. Does nothing other than
+     * calling the listUpdate() function with just the $id and $name
+     * variables.
+     *
+     * @param string $id    The gateway ID for the list to update.
+     * @param string $name  The new arbitrary name for the list.
+     *
+     * @return mixed  True on success or PEAR Error on failure.
+     */
+    function listRename($id, $name)
+    {
+        return $this->listUpdate($id, $name);
+    }
+
+    /**
+     * Fetches a listing of available distribution lists on the server.
+     *
+     * @return mixed  An array of lists on success or PEAR Error on failure.
+     *                Format of the returned lists is for example:<code>
+     *                   array(<uniqueid> => array('name'   => <name>),
+     *                         <uniqueid> => array('name'   => <name>));
+     * </code>
+     */
+    function getLists()
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postdistribution.php?username=%s&password=%s&cmd=GETCOMPACTLIST',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']));
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check if there was an error response. */
+        if (substr($response, 0, 22) != 'AQSMS-DISTRIBUTIONLIST') {
+            return $this->getError($response, _("Could not retrieve distribution lists. %s"));
+        }
+
+        /* Parse the response and construct the array. */
+        list($response, $lists_str) = explode(',', $response, 2);
+
+        /* Check that the full list of distribution lists has been received. */
+        $length = substr($response, 22);
+        if (strlen($lists_str) != $length) {
+            return PEAR::raiseError(_("Could not fetch the complete list of distribution lists."));
+        }
+        $lists_lines = explode("\n", $lists_str);
+        $lists = array();
+        /* Loop through lines and pick out the fields, make sure that the ""
+         * are not included in the values, so get the line less 1 char on each
+         * end and split for ",". */
+        foreach ($lists_lines as $line) {
+            list($id, $name, $count) = explode('","', substr($line, 1, -1));
+            $lists[$id] = array('name'  => $name,
+                                'count' => $count);
+        }
+
+        return $lists;
+    }
+
+    /**
+     * Fetches a specific distribution list from the gateway.
+     *
+     * @param string  The ID of the distribution list to fetch.
+     *
+     * @return mixed  An array of numbers in the list on success or PEAR Error
+     *                on failure.
+     */
+    function getList($id)
+    {
+        /* Set up the url and call it. */
+        $url = sprintf('postdistribution.php?username=%s&password=%s&cmd=GETNUMBERSWITHID&distid=%s',
+                       urlencode($this->_params['user']),
+                       urlencode($this->_params['password']),
+                       $id);
+        $response = $this->_callURL($url);
+        if (is_a($response, 'PEAR_Error')) {
+            return $response;
+        }
+
+        /* Check if there was an error response. */
+        if (substr($response, 0, 22) != 'AQSMS-DISTRIBUTIONLIST') {
+            return $this->getError($response, _("Could not retrieve distribution list. %s"));
+        }
+
+        /* Parse the response and construct the array. */
+        list($response, $list_str) = explode(',', $response, 2);
+
+        /* Check that the full list of distribution lists has been received. */
+        $length = substr($response, 22);
+        if (strlen($list_str) != $length) {
+            return PEAR::raiseError(_("Could not fetch complete distribution list."));
+        }
+        $list_str = trim($list_str);
+        list($count, $numbers) = explode('","', $list_str);
+
+        /* TODO: Anything useful that can be done with the count of numbers at
+         * the start? */
+        $count = substr($count, 1);
+
+        /* Explode the list of numbers into an array and return. */
+        $numbers = substr($numbers, 0, -1);
+        return explode(',', $numbers);
+    }
+
+    /**
+     * Identifies this gateway driver and returns a brief description.
+     *
+     * @return array  Array of driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("sms2email via HTTP"),
+            'desc' => _("This driver allows sending of messages through the sms2email (http://sms2email.com) gateway, using the HTTP API"),
+        );
+    }
+
+    /**
+     * Returns the required parameters for this gateway driver. The settable
+     * parameters for this gateway are:<pre>
+     *   - user            - The username for authentication on the gateway;
+     *   - password        - The password for authentication on the gateway;
+     *   - ssl             - Whether or not to use SSL for communication with
+     *                       the gateway.
+     *   - delivery_report - A URL for a script which would accept delivery
+     *                       report from the gateway.
+     * </pre>
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        $params = array();
+        $params['user']     = array('label' => _("Username"), 'type' => 'text');
+        $params['password'] = array('label' => _("Password"), 'type' => 'text');
+        $params['ssl']      = array('label'    => _("Use SSL"),
+                                    'type'     => 'boolean',
+                                    'required' => false);
+        $params['delivery_report'] = array('label'    => _("URL for your script delivery status report"),
+                                           'type'     => 'text',
+                                           'required' => false);
+
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters that can be set as default for sending messages
+     * using this gateway driver and displayed when sending messages.
+     *
+     * @return array  Array of parameters that can be set as default.
+     */
+    function getDefaultSendParams()
+    {
+        $params = array();
+        $params['from'] = array(
+            'label' => _("Source address"),
+            'type' => 'text');
+
+        $params['deliv_time'] = array(
+            'label' => _("Delivery time"),
+            'type' => 'enum',
+            'params' => array(array('now' => _("immediate"), 'user' => _("user select"))));
+
+        $types = array('SMS_TEXT' => _("Standard"), 'SMS_FLASH' => _("Flash"));
+        $params['msg_type'] = array(
+            'label' => _("Message type"),
+            'type' => 'keyval_multienum',
+            'params' => array($types));
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters for sending messages using this gateway driver,
+     * displayed when sending messages. These are filtered out using the
+     * default values set up when creating the gateway.
+     *
+     * @return array  Array of required parameters.
+     * @todo  Would be nice to use a time/date setup rather than minutes from
+     *        now for the delivery time. Upload field for ringtones/logos?
+     */
+    function getSendParams($params)
+    {
+        if (empty($params['from'])) {
+            $params['from'] = array(
+                'label' => _("Source address"),
+                'type' => 'text');
+        }
+
+        if ($params['deliv_time'] == 'user') {
+            $params['deliv_time'] = array(
+                'label' => _("Delivery time"),
+                'type' => 'int',
+                'desc' => _("Value in minutes from now."));
+        }
+
+        if (count($params['msg_type']) > 1) {
+            $params['msg_type'] = array(
+                'label' => _("Message type"),
+                'type' => 'enum',
+                'params' => array($params['msg_type']));
+        } else {
+            $params['msg_type'] = $params['msg_type'][0];
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns a string representation of an error code.
+     *
+     * @param integer $error  The error code to look up.
+     * @param string $text    An existing error text to use to raise a
+     *                        PEAR Error.
+     *
+     * @return mixed  A textual message corresponding to the error code or a
+     *                PEAR Error if passed an existing error text.
+     *
+     * @todo  Check which of these are actually required and trim down the
+     *        list.
+     */
+    function getError($error, $error_text = '')
+    {
+        $error = trim($error);
+
+        /* An array of error codes returned by the gateway. */
+        $errors = array(
+            'AQSMS-NOAUTHDETAILS'        => _("No username and/or password sent."),
+            'AQSMS-AUTHERROR'            => _("Incorrect username and/or password."),
+            'AQSMS-NOMSG'                => _("No message supplied."),
+            'AQSMS-NODEST'               => _("No destination supplied."),
+            'AQSMS-NOCREDIT'             => _("Insufficient credit."),
+            'AQSMS-NONAMESUPPLIED'       => _("No name specified."),
+            'AQSMS-NONUMBERSUPPLIED'     => _("No number specified."),
+            'AQSMS-ADDRESSBOOKERROR'     => _("There was an error performing the specified address book function. Please try again later."),
+            'AQSMS-CONTACTIDERROR'       => _("The contact ID number was not specified, left blank or was not found in the database."),
+            'AQSMS-CONTACTUPDATEERROR'   => _("There was an error updating the contact details. Please try again later."),
+            'AQSMS-DISTIDERROR'          => _("The distribution list ID was either not specified, left blank or not found in the database."),
+            'AQSMS-NODISTLISTSUPPLIED'   => _("The distribution list was not specified."),
+            'AQSMS-INSUFFICIENTCREDITS'  => _("Insufficient credit to send to the distribution list."),
+            'AQSMS-NONUMBERLISTSUPPLIED' => _("Numbers not specified for updating in distribution list."),
+            'AQSMS-DISTLISTUPDATEERROR'  => _("There was an error updating the distribution list. Please try again later."));
+
+        if (empty($error_text)) {
+            return $errors[$error];
+        } else {
+            return PEAR::raiseError(sprintf($error_text, $errors[$error]));
+        }
+    }
+
+    /**
+     * Do the http call using a url passed to the function.
+     *
+     * @access private
+     *
+     * @param string $url  The url to call.
+     *
+     * @return mixed  The response on success or PEAR Error on failure.
+     */
+    function _callURL($url)
+    {
+        $options['method'] = 'POST';
+        $options['timeout'] = 5;
+        $options['allowRedirects'] = true;
+
+        $url = (empty($this->_params['ssl']) ? 'http://' : 'https://') . $this->_base_url . $url;
+
+        $http = new HTTP_Request($url, $options);
+        @$http->sendRequest();
+        if ($http->getResponseCode() != 200) {
+            return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
+        }
+
+        return $http->getResponseBody();
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/textmagic_http.php b/framework/Net_SMS/SMS/textmagic_http.php
new file mode 100644 (file)
index 0000000..3350298
--- /dev/null
@@ -0,0 +1,544 @@
+<?php
+
+/**
+ * Net_SMS_textmagic_http Class implements the HTTP API for accessing the
+ * TextMagic (api.textmagic.com) SMS gateway.
+ *
+ * Copyright 2009 Fedyashev Nikita <nikita@realitydrivendeveloper.com>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS/textmagic_http.php,v 1.1 2009/07/17 15:27:56 chuck Exp $
+ *
+ * @package Net_SMS
+ * @author  Fedyashev Nikita <nikita@realitydrivendeveloper.com>
+ * 
+ */
+class Net_SMS_textmagic_http extends Net_SMS
+{
+    var $_base_url = 'https://www.textmagic.com/app/api?';
+
+    function Net_SMS_textmagic_http($params)
+    {
+        
+        parent::Net_SMS($params);
+        
+        if (!extension_loaded('json')) {
+            die ("JSON extenstion isn't loaded!");
+        }
+    }
+
+    /**
+     * An array of capabilities, so that the driver can report which operations
+     * it supports and which it doesn't. Possible values are:<pre>
+     *   send           - Send SMS, scheduled sending;
+     *   account        - Check account balance;
+     *   messageStatus  - Check messages's cost and delivery status;
+     *   receive        - Receive incoming messages;
+     *   deleteReply    - Delete specified incoming messages;
+     *   checkNumber    - Check phone number validity and destination price;
+     * </pre>
+     *
+     * @var array
+     */
+    var $capabilities = array('auth'           => false,
+                              'batch'          => 100,
+                              'multi'          => true,
+                              'receive'        => true,
+                              'credit'         => true,
+                              'addressbook'    => false,
+                              'lists'          => false,
+    
+                              'message_status' => true,
+                              'delete_reply'   => true,
+                              'check_number'   => true);
+
+    /**
+     * This function does the actual sending of the message.
+     *
+     * @param array  &$message The array containing the message and its send
+     *                         parameters.
+     * @param string $to       The destination string.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send(&$message, $to)
+    {
+
+        $unicode    = $this->_getUnicodeParam($message);
+        $max_length = $this->_getMaxLengthParam($message);
+        
+        $to = implode(',', $to);
+
+        $url = sprintf('cmd=send&phone=%s&text=%s&unicode=%s&max_length=%s',
+                       urlencode($to), 
+                       urlencode($message['text']),
+                       $unicode,
+                       $max_length);
+        
+        $response = $this->_callURL($url);
+        
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        $result = array();
+        
+        if (!array_key_exists('error_code', $response)) {
+            
+            if (count(explode(',', $to)) == 1) {
+                
+                $message_ids = array_keys($response['message_id']);
+                
+                $result[$to] = array(
+                    0 => 1,
+                    1 => $message_ids[0]
+                );
+            } else {
+                foreach ($response['message_id'] as $id => $recipient) {
+                    $result[$recipient] = array(1, $id);
+                }
+            }
+        } else {
+            
+            if (count(explode(',', $to)) == 1) {
+                
+                $result[$to] = array(
+                    0 => 0,
+                    1 => $response['error_message']
+                );
+            } else {
+                foreach (explode(',', $to) as $recipient) {
+                    $result[$recipient] = array(0, $response['error_message']);
+                }
+            }
+        }
+        
+        return $result;
+    }
+
+    function _getMaxLengthParam($message) {
+        $default_params = $this->getDefaultSendParams();
+        
+        if (isset($message['send_params']['max_length'])) {
+            $max_length = $message['send_params']['max_length'];
+        } else {
+            $max_length = $default_params['max_length']['default_value'];
+        }
+        
+        return $max_length;
+        
+    }
+    
+    function _getUnicodeParam($message) {
+        $default_params = $this->getDefaultSendParams();
+        
+        if (isset($message['send_params']['unicode'])) {
+            $unicode = $message['send_params']['unicode'];
+        } else {
+            $unicode = $default_params['unicode']['default_value'];
+        }
+        
+        return $unicode;
+    }
+    
+    /**
+     * This function check message delivery status.
+     *
+     * @param array $ids The array containing message IDs.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function messageStatus($ids)
+    {
+        
+        if (!is_array($ids)) {
+            $ids = array($ids);
+        }
+
+        $ids = implode(',', $ids);
+        
+        $url = sprintf('cmd=message_status&ids=%s',
+                       urlencode($ids));
+        
+        $response = $this->_callURL($url);
+        
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        $result = array();
+        
+        if (!array_key_exists('error_code', $response)) {
+            
+            if (count(explode(',', $ids)) == 1) {
+                
+                $result[$ids] = array(
+                    0 => 1,
+                    1 => $response[$ids]
+                );
+            } else {
+                foreach ($response as $id => $message) {
+                    $result[$id] = array(1, $message);
+                }
+            }
+        } else {
+            
+            if (count(explode(',', $ids)) == 1) {
+                
+                $result[$to] = array(
+                    0 => 0,
+                    1 => $response['error_message']
+                );
+            } else {
+                foreach (explode(',', $ids) as $id) {
+                    $result[$id] = array(0, $response['error_message']);
+                }
+            }
+        }
+        
+        return $result;
+    }    
+    
+    /**
+     * This function retrieves incoming messages.
+     *
+     * @param array $last_retrieved_id The array containing message IDs.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function receive($last_retrieved_id)
+    {
+        if (!is_int($last_retrieved_id)) {
+            $last_retrieved_id = int($last_retrieved_id);
+        }
+
+        $url = sprintf('cmd=receive&last=%s', $last_retrieved_id);
+        
+        $response = $this->_callURL($url);
+        
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        $result = array();
+        
+        if (!array_key_exists('error_code', $response)) {
+            $result[0] = 1;
+            
+            $result[1] = $response;
+        } else {
+            $result[0] = 0;
+            
+            $result[1] = $response['error_message'];
+        }
+        
+        return $result;
+    }    
+
+    
+    /**
+     * This function allows you to delete Incoming message
+     *
+     * @param array $ids  The array containing message IDs.
+     *
+     * @return array An array with the success status and additional
+     *                information.
+     */
+    function deleteReply($ids)
+    {
+        
+        if (!is_array($ids)) {
+            $ids = array($ids);
+        }
+
+        $ids = implode(',', $ids);
+        
+        /* Set up the http sending url. */
+        $url = sprintf('cmd=delete_reply&ids=%s',
+                       urlencode($ids));
+        
+        $response = $this->_callURL($url);
+
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        $result = array();
+
+        if (!array_key_exists('error_code', $response)) {
+            
+            if (count(explode(',', $ids)) == 1) {
+                
+                $result[$ids] = array(
+                    0 => 1,
+                    1 => true
+                );
+            } else {
+                foreach ($response['deleted'] as $id) {
+                    $result[$id] = array(1, true);
+                }
+            }
+        } else {
+            
+            if (count(explode(',', $ids)) == 1) {
+                
+                $result[$to] = array(
+                    0 => 0,
+                    1 => $response['error_message']
+                );
+            } else {
+                foreach (explode(',', $ids) as $id) {
+                    $result[$id] = array(0, $response['error_message']);
+                }
+            }
+        }
+
+        return $result;
+    }
+
+     /**
+     * This function allows you to validate phone number's format,
+     * check its country and message price to the destination . 
+     *
+     * @param array $numbers Phone numbers array to be checked.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function checkNumber($numbers)
+    {
+        
+        if (!is_array($numbers)) {
+            $numbers = array($numbers);
+        }
+
+        $numbers = implode(',', $numbers);
+        
+        $url = sprintf('cmd=check_number&phone=%s',
+                       urlencode($numbers));
+        
+        $response = $this->_callURL($url);
+
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        if (is_a($response, 'PEAR_Error')) {
+              return PEAR::raiseError(sprintf(_("Send failed.")));
+        }
+        
+        $result = array();
+        
+        if (!array_key_exists('error_code', $response)) {
+            
+            if (count(explode(',', $numbers)) == 1) {
+                
+                $result[$numbers] = array(
+                    0 => 1,
+                    1 => array(
+                        "price" => $response[$numbers]['price'],
+                        "country" => $response[$numbers]['country'] 
+                    )
+                );
+            } else {
+                foreach (explode(',', $numbers) as $number) {
+                    $result[$number] = array(1, array(
+                        "price" => $response[$number]['price'],
+                        "country" => $response[$number]['country'] 
+                    ));
+                }
+            }
+        } else {
+            
+            if (count(explode(',', $numbers)) == 1) {
+                
+                $result[explode(',', $numbers)] = array(
+                    0 => 0,
+                    1 => $response['error_message']
+                );
+            } else {    
+                foreach (explode(',', $numbers) as $number) {
+                    $result[$number] = array(0, $response['error_message']);
+                }
+            }
+        }
+        
+        return $result;
+        
+    }     
+    
+    /**
+     * Returns the current credit balance on the gateway.
+     *
+     * @access private
+     *
+     * @return integer  The credit balance available on the gateway.
+     */
+    function _getBalance()
+    {
+        $url = 'cmd=account';
+        
+        $response = $this->_callURL($url);
+        
+        if (is_a($response, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Send failed. %s"), $response['error_message']));
+        }
+        
+        if (!array_key_exists('error_code', $response)) {
+            return $response['balance'];
+        } else {
+            return $this->getError($response['error_message'], _("Could not check balance. %s"));
+        }
+    }
+
+    /**
+     * Identifies this gateway driver and returns a brief description.
+     *
+     * @return array  Array of driver info.
+     */
+    function getInfo()
+    {
+        $info['name'] = _("TextMagic via HTTP");
+        $info['desc'] = _("This driver allows sending of messages through the TextMagic (http://api.textmagic.com) gateway, using the HTTP API");
+
+        return $info;
+    }
+
+    /**
+     * Returns the required parameters for this gateway driver.
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        $params               = array();
+        $params['username']   = array('label' => _("Username"), 'type' => 'text');
+        $params['password']   = array('label' => _("Password"), 'type' => 'text');
+        $params['unicode']    = array('label' => _("Unicode message flag"), 'type' => 'int');
+        $params['max_length'] = array('label' => _("Maximum messages to be sent at once"), 'type' => 'int');
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters that can be set as default for sending messages
+     * using this gateway driver and displayed when sending messages.
+     *
+     * @return array  Array of parameters that can be set as default.
+     * @todo  Set up batch fields/params, would be nice to have ringtone/logo
+     *        support too, queue choice, unicode choice.
+     */
+    function getDefaultSendParams()
+    {
+        $params = array();
+
+        $params['max_length'] = array(
+            'label' => _("Max messages quantity"),
+            'type' => 'int',
+            'default_value' => 3);
+
+        $params['unicode'] = array(
+            'label' => _("Unicode message flag"),
+            'type' => 'int',
+            'default_value' => 1);
+        
+        return $params;
+    }
+
+    /**
+     * Returns the parameters for sending messages using this gateway driver,
+     * displayed when sending messages. These are filtered out using the
+     * default values set for the gateway.
+     *
+     * @return array  Array of required parameters.
+     */
+    function getSendParams($params)
+    {
+        if (empty($params['max_length'])) {
+            $params['max_length'] = array(
+                'label' => _("Max messages quantity"),
+                'type' => 'int');
+        }
+        
+        if (empty($params['unicode'])) {
+            $params['unicode'] = array(
+                'label' => _("Unicode message flag"),
+                'type' => 'int');
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns a string representation of an error code.
+     *
+     * @param string  $error_text An existing error text to use to raise a
+     *                            PEAR Error.
+     * @param integer $error The error code to look up.
+     *
+     * @return mixed  A textual message corresponding to the error code or a
+     *                PEAR Error if passed an existing error text.
+     *
+     * @todo  Check which of these are actually required and trim down the
+     *        list.
+     */
+    function getError($error_text = '', $error)
+    {
+        /* An array of error codes returned by the gateway. */
+        $errors = array(2  => _("Low balance"),
+                        5  => _("Invalid username/password combination"),
+                        6  => _("Message was not sent"),
+                        7  => _("Too long message length"),
+                        8  => _("IP address is not allowed"),
+                        9  => _("Wrong phone number format"),
+                        10 => _("Wrong parameter value"),
+                        11 => _("Daily API requests limit exceeded"),
+                        12 => _("Too many items per request"),
+                        13 => _("Your account has been deactivated"),
+                        14 => _("Unknwon message ID"),
+                        15 => _("Unicode characters detected on unicode=0 option"));
+        
+        if (!empty($error_text)) {
+            return $error_text;
+        } else {
+            return PEAR::raiseError($errors[$error], $error);
+        }
+    }
+
+    /**
+     * Do the http call using a url passed to the function.
+     *
+     * @param string $url The url to call.
+     *
+     * @return mixed  The response on success or PEAR Error on failure.
+     */
+    function _callURL($url)
+    {
+        $options['method']         = 'POST';
+        $options['timeout']        = 5;
+        $options['allowRedirects'] = true;
+
+        if (!@include_once 'HTTP/Request.php') {
+            return PEAR::raiseError(_("Missing PEAR package HTTP_Request."));
+        }
+        $http = &new HTTP_Request($this->_base_url . $url, $options);
+        
+        /* Add the authentication values to POST. */
+        $http->addPostData('username', $this->_params['user']);
+        $http->addPostData('password', $this->_params['password']);
+        
+        
+        @$http->sendRequest();
+        if ($http->getResponseCode() != 200) {
+            return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
+        }
+
+        return json_decode($http->getResponseBody(), true);
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/vodafoneitaly_smtp.php b/framework/Net_SMS/SMS/vodafoneitaly_smtp.php
new file mode 100644 (file)
index 0000000..5439205
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/**
+ * Net_SMS_vodafoneitaly_smtp Class implements the SMTP API for accessing the
+ * Vodafone Italy SMS gateway. Use of this gateway requires an email account
+ * with Vodafone Italy (www.190.it).
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ * Copyright 2003-2007 Matteo Zambelli <mmzambe@hotmail.com>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS/vodafoneitaly_smtp.php,v 1.26 2009/01/06 17:49:34 jan Exp $
+ *
+ * @author Marko Djukic <marko@oblo.com>
+ * @author Matteo Zambelli <mmzambe@hotmail.com>
+ * @package Net_SMS
+ */
+class Net_SMS_vodafoneitaly_smtp extends Net_SMS {
+
+    /**
+     * An array of capabilities, so that the driver can report which operations
+     * it supports and which it doesn't. Possible values are:<pre>
+     *   auth        - The gateway require authentication before sending;
+     *   batch       - Batch sending is supported;
+     *   multi       - Sending of messages to multiple recipients is supported;
+     *   receive     - Whether this driver is capable of receiving SMS;
+     *   credit      - Is use of the gateway based on credits;
+     *   addressbook - Are gateway addressbooks supported;
+     *   lists       - Gateway support for distribution lists.
+     * </pre>
+     *
+     * @var array
+     */
+    var $capabilities = array('auth'        => false,
+                              'batch'       => false,
+                              'multi'       => false,
+                              'receive'     => false,
+                              'credit'      => false,
+                              'addressbook' => false,
+                              'lists'       => false);
+
+    /**
+     * This function does the actual sending of the message.
+     *
+     * @access private
+     *
+     * @param array $message  The array containing the message and its send
+     *                        parameters.
+     * @param string $to      The recipient.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        /* Since this only works for Italian numbers, this is hardcoded. */
+        if (preg_match('/^.*?<?(\+?39)?(\d{10})>?/', $to, $matches)) {
+            $headers['From'] = $this->_params['user'];
+            $to = $matches[2] . '@sms.vodafone.it';
+
+            $mailer = Mail::factory('mail');
+            $result = $mailer->send($to, $headers, $message['text']);
+            if (is_a($result, 'PEAR_Error')) {
+                return array(0, $result->getMessage());
+            } else {
+                return array(1, null);
+            }
+        } else {
+            return array(0, _("You need to provide an Italian phone number"));
+        }
+    }
+
+    /**
+     * Identifies this gateway driver and returns a brief description.
+     *
+     * @return array  Array of driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("Vodafone Italy via SMTP"),
+            'desc' => _("This driver allows sending of messages via SMTP through the Vodafone Italy gateway, only to Vodafone numbers. It requires an email account with Vodafone Italy (http://www.190.it)."),
+        );
+    }
+
+    /**
+     * Returns the required parameters for this gateway driver.
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        return array('user' => array('label' => _("Username"),
+                                     'type' => 'text'));
+    }
+
+    /**
+     * Returns the parameters that can be set as default for sending messages
+     * using this gateway driver and displayed when sending messages.
+     *
+     * @return array  Array of parameters that can be set as default.
+     */
+    function getDefaultSendParams()
+    {
+        return array();
+    }
+
+    /**
+     * Returns the parameters for sending messages using this gateway driver,
+     * displayed when sending messages. These are filtered out using the
+     * default values set up when creating the gateway.
+     *
+     * @return array  Array of required parameters.
+     * @todo  Would be nice to use a time/date setup rather than minutes from
+     *        now for the delivery time. Upload field for ringtones/logos?
+     */
+    function getSendParams($params)
+    {
+        return array();
+    }
+
+    /**
+     * Returns a string representation of an error code.
+     *
+     * @param integer $error  The error code to look up.
+     * @param string $text    An existing error text to use to raise a
+     *                        PEAR Error.
+     *
+     * @return mixed  A textual message corresponding to the error code or a
+     *                PEAR Error if passed an existing error text.
+     *
+     * @todo  Check which of these are actually required and trim down the
+     *        list.
+     */
+    function getError($error, $error_text = '')
+    {
+    }
+
+}
diff --git a/framework/Net_SMS/SMS/win_http.php b/framework/Net_SMS/SMS/win_http.php
new file mode 100644 (file)
index 0000000..4633d66
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+/**
+ * @package Net_SMS
+ */
+
+/**
+ * HTTP_Request class.
+ */
+include_once 'HTTP/Request.php';
+
+/**
+ * Net_SMS_win_http Class implements the HTTP API for accessing the WIN
+ * (www.winplc.com) SMS gateway.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/Net_SMS/SMS/win_http.php,v 1.18 2009/01/06 17:49:34 jan Exp $
+ *
+ * @author Marko Djukic <marko@oblo.com>
+ * @package Net_SMS
+ */
+class Net_SMS_win_http extends Net_SMS {
+
+    var $_base_url = 'gateway3.go2mobile.net:10030/gateway/v3/gateway.aspx';
+
+    /**
+     * An array of capabilities, so that the driver can report which operations
+     * it supports and which it doesn't. Possible values are:<pre>
+     *   auth        - The gateway requires authentication before sending;
+     *   batch       - Batch sending is supported;
+     *   multi       - Sending of messages to multiple recipients is supported;
+     *   receive     - Whether this driver is capable of receiving SMS;
+     *   credit      - Is use of the gateway based on credits;
+     *   addressbook - Are gateway addressbooks supported;
+     *   lists       - Gateway support for distribution lists.
+     * </pre>
+     *
+     * @var array
+     */
+    var $capabilities = array('auth'        => false,
+                              'batch'       => 100,
+                              'multi'       => true,
+                              'receive'     => false,
+                              'credit'      => false,
+                              'addressbook' => false,
+                              'lists'       => false);
+
+    /**
+     * This function does the actual sending of the message.
+     *
+     * @access private
+     *
+     * @param array $message  The array containing the message and its send
+     *                        parameters.
+     * @param array $to       The destination string.
+     *
+     * @return array  An array with the success status and additional
+     *                information.
+     */
+    function _send($message, $to)
+    {
+        /* Start the XML. */
+        $xml = '<SMSMESSAGE><TEXT>' . $message['text'] . '</TEXT>';
+
+        /* Check if source from is set. */
+        if (!empty($message['send_params']['from'])) {
+            $xml .= '<SOURCE_ADDR>' . $message['send_params']['from'] . '</SOURCE_ADDR>';
+        }
+
+        /* Loop through recipients and do some minimal validity checking. */
+        if (is_array($to)) {
+            foreach ($to as $key => $val) {
+                if (preg_match('/^.*?<?(\+?\d{7,})(>|$)/', $val, $matches)) {
+                    $to[$key] = $matches[1];
+                } else {
+                    /* If a recipient is invalid stop all further sending. */
+                    return array(0, sprintf(_("Invalid recipient: \"%s\""), $val));
+                }
+            }
+
+            $to = implode('</DESTINATION_ADDR><DESTINATION_ADDR>', $to);
+        } else {
+            if (preg_match('/^.*?<?(\+?\d{7,})(>|$)/', $to, $matches)) {
+                $to = $matches[1];
+            } else {
+                return array(0, sprintf(_("Invalid recipient: \"%s\""), $to));
+            }
+        }
+        $xml .= '<DESTINATION_ADDR>' . $to . '</DESTINATION_ADDR>';
+
+        /* TODO: Should we have something more intelligent? Could actually
+         * be part of send parameters. */
+        $xml .= '<TRANSACTIONID>' . time() . '</TRANSACTIONID>';
+
+        /* TODO: Add some extra tags, just tacked on for now. */
+        $xml .= '<TYPEID>2</TYPEID><SERVICEID>1</SERVICEID></SMSMESSAGE>';
+
+        /* Send this message. */
+        $response = $this->_post($xml);
+        if (is_a($response, 'PEAR_Error')) {
+            return array(0, $response->getMessage());
+        }
+
+        /* Parse the response, check for new lines in case of multiple
+         * recipients. */
+        $lines = explode("\n", $response);
+        $response = array();
+
+        if (count($lines) > 1) {
+            /* Multiple recipients. */
+            foreach ($lines as $line) {
+                $parts = explode('To:', $line);
+                $recipient = trim($parts[1]);
+                if ($lines[0] == 'AQSMS-OK') {
+                    $response[$recipient] = array(1, null);
+                } else {
+                    $response[$recipient] = array(0, $lines[0]);
+                }
+            }
+        } else {
+            /* Single recipient. */
+            if ($lines[0] == 'AQSMS-OK') {
+                $response[$to] = array(1, null);
+            } else {
+                $response[$to] = array(0, $lines[0]);
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * Identifies this gateway driver and returns a brief description.
+     *
+     * @return array  Array of driver info.
+     */
+    function getInfo()
+    {
+        return array(
+            'name' => _("WIN via HTTP"),
+            'desc' => _("This driver allows sending of messages through the WIN (http://winplc.com) gateway, using the HTTP API"),
+        );
+    }
+
+    /**
+     * Returns the required parameters for this gateway driver. The settable
+     * parameters for this gateway are:
+     *   - user            - The username for authentication on the gateway;
+     *   - password        - The password for authentication on the gateway;
+     *
+     * @return array  Array of required parameters.
+     */
+    function getParams()
+    {
+        $params = array();
+        $params['user']     = array('label' => _("Username"), 'type' => 'text');
+        $params['password'] = array('label' => _("Password"), 'type' => 'text');
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters that can be set as default for sending messages
+     * using this gateway driver and displayed when sending messages.
+     *
+     * @return array  Array of parameters that can be set as default.
+     */
+    function getDefaultSendParams()
+    {
+        $params = array();
+        $params['from'] = array(
+            'label' => _("Source address"),
+            'type' => 'text');
+
+        $params['cost_id'] = array(
+            'label' => _("Cost ID"),
+            'type' => 'int');
+
+        return $params;
+    }
+
+    /**
+     * Returns the parameters for sending messages using this gateway driver,
+     * displayed when sending messages. These are filtered out using the
+     * default values set up when creating the gateway.
+     *
+     * @return array  Array of required parameters.
+     * @todo  Would be nice to use a time/date setup rather than minutes from
+     *        now for the delivery time. Upload field for ringtones/logos?
+     */
+    function getSendParams($params)
+    {
+        if (empty($params['from'])) {
+            $params['from'] = array(
+                'label' => _("Source address"),
+                'type' => 'text');
+        }
+
+        if (empty($params['cost_id'])) {
+            $params['deliv_time'] = array(
+                'label' => _("Cost ID"),
+                'type' => 'int');
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns a string representation of an error code.
+     *
+     * @param integer $error  The error code to look up.
+     * @param string $text    An existing error text to use to raise a
+     *                        PEAR Error.
+     *
+     * @return mixed  A textual message corresponding to the error code or a
+     *                PEAR Error if passed an existing error text.
+     *
+     * @todo  Check which of these are actually required and trim down the
+     *        list.
+     */
+    function getError($error, $error_text = '')
+    {
+        $error = trim($error);
+
+        /* An array of error codes returned by the gateway. */
+        $errors = array(
+            'AQSMS-NOAUTHDETAILS'        => _("No username and/or password sent."),
+            'AQSMS-DISTLISTUPDATEERROR'  => _("There was an error updating the distribution list. Please try again later."));
+
+        if (empty($error_text)) {
+            return $errors[$error];
+        } else {
+            return PEAR::raiseError(sprintf($error_text, $errors[$error]));
+        }
+    }
+
+    /**
+     * Do the http call using a url passed to the function.
+     *
+     * @access private
+     *
+     * @param string $xml  The XML information passed to the gateway.
+     *
+     * @return mixed  The response on success or PEAR Error on failure.
+     */
+    function _post($xml)
+    {
+        $options['method'] = 'POST';
+        $options['timeout'] = 5;
+        $options['allowRedirects'] = true;
+
+        /* Wrap the xml with the standard tags. */
+        $xml = '<?xml version="1.0" standalone="no"?><!DOCTYPE WIN_DELIVERY_2_SMS SYSTEM "winbound_messages_v1.dtd"><WIN_DELIVERY_2_SMS>' . $xml . '</WIN_DELIVERY_2_SMS>';
+
+        $http = new HTTP_Request($this->_base_url, $options);
+
+        /* Add the authentication values to POST. */
+        $http->addPostData('User', $this->_params['user']);
+        $http->addPostData('Password', $this->_params['password']);
+
+        /* Add the XML and send the request. */
+        $http->addPostData('WIN_XML', $xml);
+        @$http->sendRequest();
+        if ($http->getResponseCode() != 200) {
+            return PEAR::raiseError(sprintf(_("Could not open %s."), $this->_base_url));
+        }
+
+        return $http->getResponseBody();
+    }
+
+}
diff --git a/framework/Net_SMS/package.xml b/framework/Net_SMS/package.xml
new file mode 100644 (file)
index 0000000..c2dd1ab
--- /dev/null
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Net_SMS</name>
+ <channel>pear.php.net</channel>
+ <summary>SMS functionality.</summary>
+ <description>This package provides SMS functionality and access to SMS gateways.
+  
+ </description>
+ <lead>
+  <name>Marko Djukic</name>
+  <user>mdjukic</user>
+  <email>mdjukic@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>yunosh</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chagenbu</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-12-31</date>
+ <version>
+  <release>0.2.0</release>
+  <api>0.2.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Added generic SMPP driver (Ian Eure, Request #5101).
+* Fixed return values of the generic SMTP driver.
+* Add textmagic_http driver (loci.master@gmail.com, Request #8439).
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="SMS">
+    <file baseinstalldir="/Net" name="clickatell_http.php" role="php" />
+    <file baseinstalldir="/Net" name="generic_smpp.php" role="php" />
+    <file baseinstalldir="/Net" name="generic_smtp.php" role="php" />
+    <file baseinstalldir="/Net" name="sms2email_http.php" role="php" />
+    <file baseinstalldir="/Net" name="textmagic_http.php" role="php" />
+    <file baseinstalldir="/Net" name="vodafoneitaly_smtp.php" role="php" />
+    <file baseinstalldir="/Net" name="win_http.php" role="php" />
+   </dir> <!-- //SMS -->
+   <file baseinstalldir="/Net" name="SMS.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </required>
+  <optional>
+   <package>
+    <name>HTTP_Request</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <package>
+    <name>Mail</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <package>
+    <name>Net_SMPP_Client</name>
+    <channel>pear.php.net</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-04-12</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Fixed AT&amp;T and Cingular SMTP gateways (Horde Bug #4139).
+* Fixed warnings in sms2email and clickatell drivers.
+* Added WIN driver.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-04-14</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Added generic SMTP driver (Ian Eure).
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-06-04</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Reflection/lib/Horde/Reflection.php b/framework/Reflection/lib/Horde/Reflection.php
new file mode 100644 (file)
index 0000000..52f3a41
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * The Horde_Reflection class provides reflection methods, e.g. to generate
+ * method documentation.
+ *
+ * Based on the PEAR XML_RPC2_Server_Method class by Sergio Carvalho
+ *
+ * $Horde: framework/Reflection/lib/Horde/Reflection.php,v 1.4 2009/06/09 23:23:44 slusarz Exp $
+ *
+ * Copyright 2004-2006 Sergio Gonalves Carvalho
+ *                     (<sergio.carvalho@portugalmail.com>)
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Sergio Carvalho <sergio.carvalho@portugalmail.com>
+ * @author  Duck <duck@obala.net>
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Reflection
+ */
+abstract class Horde_Reflection {
+
+    /**
+     * Method signature parameters.
+     *
+     * @var array
+     */
+    protected $_parameters;
+
+    /**
+     * Method signature return type.
+     *
+     * @var string
+     */
+    protected $_returns;
+
+    /**
+     * Method help, for introspection.
+     *
+     * @var string
+     */
+    protected $_help;
+
+    /**
+     * Number of required parameters.
+     *
+     * @var integer
+     */
+    protected $_numberOfRequiredParameters;
+
+    /**
+     * External method name.
+     *
+     * @var string
+     */
+    protected $_name;
+
+    /**
+     * Constructor.
+     *
+     * @param ReflectionMethod $method  The PHP method to introspect.
+     */
+    public function __construct(ReflectionFunction $method)
+    {
+        $docs = $method->getDocComment();
+        $docs = explode("\n", $docs);
+
+        $parameters = array();
+        $returns = 'mixed';
+        $shortdesc = '';
+        $paramcount = -1;
+        $this->_name = $method->getName();
+
+        // Extract info from docblock.
+        $paramDocs = array();
+        foreach ($docs as $i => $doc) {
+            $doc = trim($doc, " \r\t/*");
+            if (strlen($doc) && strpos($doc, '@') !== 0) {
+                if ($shortdesc) {
+                    $shortdesc .= "\n";
+                }
+                $shortdesc .= $doc;
+                continue;
+            }
+            if (strpos($doc, '@param') === 0) {
+                // Save doctag for usage later when filling parameters.
+                $paramDocs[] = $doc;
+            }
+
+            if (strpos($doc, '@return') === 0) {
+                $param = preg_split("/\s+/", $doc);
+                if (isset($param[1])) {
+                    $param = $param[1];
+                    $returns = $param;
+                }
+            }
+        }
+
+        // We don't use isOptional() because of bugs in the reflection API.
+        $this->_numberOfRequiredParameters = $method->getNumberOfRequiredParameters();
+        // Fill in info for each method parameter.
+        foreach ($method->getParameters() as $parameterIndex => $parameter) {
+            // Parameter defaults.
+            $newParameter = array('type' => 'mixed');
+
+            // Attempt to extract type and doc from docblock.
+            if (array_key_exists($parameterIndex, $paramDocs) &&
+                preg_match('/@param\s+(\S+)(\s+(.+))/',
+                           $paramDocs[$parameterIndex],
+                           $matches)) {
+                if (strpos($matches[1], '|')) {
+                    $newParameter['type'] = self::_limitPHPType(explode('|', $matches[1]));
+                } else {
+                    $newParameter['type'] = self::_limitPHPType($matches[1]);
+                }
+                $tmp = '$' . $parameter->getName() . ' ';
+                if (strpos($matches[2], '$' . $tmp) === 0) {
+                    $newParameter['doc'] = $matches[2];
+                } else {
+                    // The phpdoc comment is something like "@param string
+                    // $param description of param". Let's keep only
+                    // "description of param" as documentation (remove
+                    // $param).
+                    $newParameter['doc'] = substr($matches[2], strlen($tmp));
+                }
+            }
+
+            $parameters[$parameter->getName()] = $newParameter;
+        }
+
+        $this->_parameters = $parameters;
+        $this->_returns  = $returns;
+        $this->_help = $shortdesc;
+    }
+
+    /**
+     * Returns a complete description of the method.
+     *
+     * @return string  The method documentation.
+     */
+    abstract public function autoDocument();
+
+    /**
+     * Converts types from phpdoc comments (and limit to xmlrpc available
+     * types) to php type names.
+     *
+     * @var string|array $type  One or multiple phpdoc comment type(s).
+     *
+     * @return string|array  The standardized php type(s).
+     */
+    protected static function _limitPHPType($type)
+    {
+        $convertArray = array(
+                'int' => 'integer',
+                'i4' => 'integer',
+                'integer' => 'integer',
+                'string' => 'string',
+                'str' => 'string',
+                'char' => 'string',
+                'bool' => 'boolean',
+                'boolean' => 'boolean',
+                'array' => 'array',
+                'float' => 'double',
+                'double' => 'double',
+                'array' => 'array',
+                'struct' => 'array',
+                'assoc' => 'array',
+                'structure' => 'array',
+                'datetime' => 'mixed',
+                'datetime.iso8601' => 'mixed',
+                'iso8601' => 'mixed',
+                'base64' => 'string'
+            );
+
+
+        if (is_array($type)) {
+            $types = array();
+            foreach ($type as $tmp) {
+                $tmp = Horde_String::lower($tmp);
+                if (isset($convertArray[$tmp])) {
+                    $types[] = $convertArray[$tmp];
+                } else {
+                    $types[] = 'mixes';
+                }
+            }
+            return $types;
+        } else {
+            $tmp = Horde_String::lower($type);
+            if (isset($convertArray[$tmp])) {
+                return $convertArray[$tmp];
+            }
+        }
+
+        return 'mixed';
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Document instance based on $driver.
+     *
+     * @param string $function  The method to document.
+     * @param string $driver    The type of the concrete Horde_Document
+     *                          subclass to return. The class name is based on
+     *                          the driver.  The code is dynamically included.
+     *
+     * @return Horde_Document  The newly created concrete Horde_Document
+     *                         instance, or false on an error.
+     */
+    public static function factory($function, $driver = 'Html')
+    {
+        $class = 'Horde_Reflection_' . $driver;
+        if (!class_exists($class)) {
+            include dirname(__FILE__) . '/Reflection/' . $driver . '.php';
+        }
+        if (class_exists($class)) {
+            return new $class($function);
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/framework/Reflection/lib/Horde/Reflection/Cli.php b/framework/Reflection/lib/Horde/Reflection/Cli.php
new file mode 100644 (file)
index 0000000..ba5a9ab
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * The Horde_Reflection_Cli class renders method documention on the command line.
+ *
+ * Based on the PEAR XML_RPC2_Server_Method class by Sergio Carvalho
+ *
+ * $Horde: framework/Reflection/lib/Horde/Reflection/Cli.php,v 1.3 2009/06/10 19:57:54 slusarz Exp $
+ *
+ * Copyright 2004-2006 Sergio Gonalves Carvalho
+ *                     (<sergio.carvalho@portugalmail.com>)
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Sergio Carvalho <sergio.carvalho@portugalmail.com>
+ * @author  Duck <duck@obala.net>
+ * @package Horde_Reflection
+ */
+class Horde_Reflection_CLI extends Horde_Reflection {
+
+    /**
+     * Cli inteface
+     */
+    private $_cli;
+
+    /**
+     * Constructor.
+     *
+     * @param ReflectionMethod $method  The PHP method to introspect.
+     */
+    public function __construct(ReflectionFunction $method)
+    {
+        Horde_Cli::init();
+        $this->_cli = Horde_Cli::singleton();
+
+        parent::__construct($method);
+    }
+
+    /**
+     * Returns a signature of the method.
+     *
+     * @return string  Method signature.
+     */
+    private function _getSignature()
+    {
+        $name = $this->_name;
+        $returnType = $this->_returns;
+
+        $title = substr($name, strpos($name, '_', 2) + 1);
+
+        $result = $this->_cli->yellow($title) . '  ' .  $this->_help . "\n";
+        $result .= $this->_cli->blue($returnType) . ' ';
+        $result .=  $this->_cli->green($title) . ' ';
+        $result .= "(";
+        $first = true;
+        $nbr = 0;
+
+        while (list($name, $parameter) = each($this->_parameters)) {
+            $nbr++;
+            if ($nbr == $this->_numberOfRequiredParameters + 1) {
+                $result .= " [ ";
+            }
+            if ($first) {
+                $first = false;
+            } else {
+                $result .= ', ';
+            }
+            $type = $parameter['type'];
+            $result .= $this->_cli->red($type) . ' ';
+            $result .= $this->_cli->blue($name);
+        }
+        reset($this->_parameters);
+        if ($nbr > $this->_numberOfRequiredParameters) {
+            $result .= " ] ";
+        }
+        $result .= ")";
+        return $result;
+    }
+
+    /**
+     * Returns a complete description of the method.
+     *
+     * @return string  A snippet with the method documentation.
+     */
+    public function autoDocument()
+    {
+        $this->_cli->writeln();
+        $this->_cli->writeln($this->_getSignature());
+
+        if (count($this->_parameters) > 0) {
+            $out = $this->_cli->indent("Type\tName\tDocumentation\n");
+            while (list($name, $parameter) = each($this->_parameters)) {
+                $type = $parameter['type'];
+                if (is_array($type)) {
+                    $type = implode(' | ', $type);
+                }
+                if (isset($parameter['doc'])) {
+                    $doc = trim($parameter['doc']);
+                } else {
+                    $doc = '';
+                }
+                $out .= $this->_cli->indent("$type\t$name\t$doc\n");
+            }
+            $this->_cli->writeln($out);
+
+            reset($this->_parameters);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Reflection/lib/Horde/Reflection/Html.php b/framework/Reflection/lib/Horde/Reflection/Html.php
new file mode 100644 (file)
index 0000000..f7e661f
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+/**
+ * The Horde_Reflection_Html class renders method documention in the HTML
+ * format.
+ *
+ * Based on the PEAR XML_RPC2_Server_Method class by Sergio Carvalho
+ *
+ * $Horde: framework/Reflection/lib/Horde/Reflection/Html.php,v 1.2 2009/01/06 17:49:40 jan Exp $
+ *
+ * Copyright 2004-2006 Sergio Gonalves Carvalho
+ *                     (<sergio.carvalho@portugalmail.com>)
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Sergio Carvalho <sergio.carvalho@portugalmail.com>
+ * @author  Duck <duck@obala.net>
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Reflection
+ */
+class Horde_Reflection_Html extends Horde_Reflection {
+
+    /**
+     * Returns a signature of the method.
+     *
+     * @return string  Method signature.
+     */
+    private function _getSignature()
+    {
+        $name = $this->_name;
+        $returnType = $this->_returns;
+        $result  = "<span class=\"type\">($returnType)</span> ";
+        $result .= "<span class=\"name\">$name</span>";
+        $result .= "<span class=\"other\">(</span>";
+        $first = true;
+        $nbr = 0;
+        while (list($name, $parameter) = each($this->_parameters)) {
+            $nbr++;
+            if ($nbr == $this->_numberOfRequiredParameters + 1) {
+                $result .= "<span class=\"other\">[</span>";
+            }
+            if ($first) {
+                $first = false;
+            } else {
+                $result .= ', ';
+            }
+            $type = $parameter['type'];
+            $result .= "<span class=\"paratype\">($type) </span>";
+            $result .= "<span class=\"paraname\">$name</span>";
+        }
+        reset($this->_parameters);
+        if ($nbr > $this->_numberOfRequiredParameters) {
+            $result .= "<span class=\"other\">]</span>";
+        }
+        $result .= "<span class=\"other\">)</span>";
+        return $result;
+    }
+
+    /**
+     * Returns a complete HTML description of the method.
+     *
+     * @return string  A HTML snippet with the method documentation.
+     */
+    public function autoDocument()
+    {
+        $signature = $this->_getSignature();
+        $id = md5($this->_name);
+        $help = nl2br(htmlentities($this->_help));
+        $html = $this->_header();
+        $html .= "  <h3><a name=\"$id\">$signature</a></h3>\n";
+        $html .= "      <p><b>Description :</b></p>\n";
+        $html .= "      <div class=\"description\">\n";
+        $html .= "        $help\n";
+        $html .= "      </div>\n";
+        if (count($this->_parameters)>0) {
+            $html .= "      <p><b>Parameters : </b></p>\n";
+            if (count($this->_parameters)>0) {
+                $html .= "      <table>\n";
+                $html .= "        <tr><td><b>Type</b></td><td><b>Name</b></td><td><b>Documentation</b></td></tr>\n";
+                while (list($name, $parameter) = each($this->_parameters)) {
+                    $type = $parameter['type'];
+                    if (is_array($type)) {
+                        $type = implode(' | ', $type);
+                    }
+                    if (isset($parameter['doc'])) {
+                        $doc = htmlentities($parameter['doc']);
+                    } else {
+                        $doc = '';
+                        echo 'Missing doc for ' . $this->_name . '<br />';
+                    }
+                    $html .= "        <tr><td>$type</td><td>$name</td><td>$doc</td></tr>\n";
+                }
+                reset($this->_parameters);
+                $html .= "      </table>\n";
+            }
+        }
+        $html .= $this->_footer();
+
+        return $html;
+    }
+
+    private function _header()
+    {
+        $html = '
+        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+        <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+        <head>
+            <meta http-equiv="Content-Type" content="text/HTML; charset=UTF-8"  />
+            <title>Available XMLRPC methods for this server</title>
+            <style type="text/css">
+            li,p { font-size: 10pt; font-family: Arial,Helvetia,sans-serif; }
+            a:link { background-color: white; color: blue; text-decoration: underline; font-weight: bold; }
+            a:visited { background-color: white; color: blue; text-decoration: underline; font-weight: bold; }
+            table { border-collapse:collapse; width: 100% }
+            table,td { padding: 5px; border: 1px solid black; }
+            div.bloc { border: 1px dashed gray; padding: 10px; margin-bottom: 20px; }
+            div.description { border: 1px solid black; padding: 10px; }
+            span.type { background-color: white; color: gray; font-weight: normal; }
+            span.paratype { background-color: white; color: gray; font-weight: normal; }
+            span.name { background-color: white; color: #660000; }
+            span.paraname { background-color: white; color: #336600; }
+            img { border: 0px; }
+            li { font-size: 12pt; }
+            </style>
+        </head>
+        <body>
+        ';
+
+        return $html;
+    }
+
+    private function _footer()
+    {
+        return '  </body></html>';
+    }
+
+}
+
diff --git a/framework/Reflection/lib/Horde/Reflection/Wiki.php b/framework/Reflection/lib/Horde/Reflection/Wiki.php
new file mode 100644 (file)
index 0000000..076b817
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * The Horde_Reflection_Wiki class renders method documention in the Text_Wiki
+ * format.
+ *
+ * $Horde: framework/Reflection/lib/Horde/Reflection/Wiki.php,v 1.3 2009/01/06 17:49:40 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Duck <duck@obala.net>
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Horde_Reflection
+ */
+class Horde_Reflection_Wiki extends Horde_Reflection {
+
+    /**
+     * Returns a signature of the method.
+     *
+     * @return string  Method signature.
+     */
+    private function _getSignature()
+    {
+        $name = $this->_name;
+        $returnType = $this->_returns;
+
+        $title = substr($this->_name, strpos($name, '_', 2) + 1);
+        $desc = substr($this->_help, 0, 20) . '...';
+
+        $result = "++$title - $desc\n";
+        $result .= "##gray|($returnType)## ";
+        $result .= "##660000|$title##";
+        $result .= "(";
+        $first = true;
+        $nbr = 0;
+        while (list($name, $parameter) = each($this->_parameters)) {
+            $nbr++;
+            if ($nbr == $this->_numberOfRequiredParameters + 1) {
+                $result .= " [ ";
+            }
+            if ($first) {
+                $first = false;
+            } else {
+                $result .= ', ';
+            }
+            $type = $parameter['type'];
+            $result .= "##gray|($type) ##";
+            $result .= "##336600|$name##";
+        }
+        reset($this->_parameters);
+        if ($nbr > $this->_numberOfRequiredParameters) {
+            $result .= " ] ";
+        }
+        $result .= ")";
+        return $result;
+    }
+
+    /**
+     * Returns a complete wiki description of the method.
+     *
+     * @return string  A wiki snippet with the method documentation.
+     */
+    public function autoDocument()
+    {
+        $signature = $this->_getSignature();
+        $id = md5($this->_name);
+        $help = trim(strip_tags($this->_help));
+
+        $html = "$signature\n";
+        if ($help) {
+            $html .= "\nDescription : \n<code>\n$help\n</code>\n";
+        }
+
+        if (count($this->_parameters)>0) {
+            $html .= "Parameters: \n";
+            if (count($this->_parameters)>0) {
+
+                $html .= "||~ Type||~ Name||~ Documentation||\n";
+                while (list($name, $parameter) = each($this->_parameters)) {
+                    $type = $parameter['type'];
+                    if (is_array($type)) {
+                        $type = implode(' | ', $type);
+                    }
+                    if (isset($parameter['doc'])) {
+                        $doc = htmlentities($parameter['doc']);
+                    } else {
+                        $doc = '';
+                        echo 'Missing doc for ' . $this->_name . '<br />';
+                    }
+                    $html .= "||$type||$name||$doc||\n";
+                }
+                reset($this->_parameters);
+            }
+        }
+
+        return $html;
+    }
+
+}
diff --git a/framework/Reflection/package.xml b/framework/Reflection/package.xml
new file mode 100644 (file)
index 0000000..2597e6c
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.7.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+    http://pear.php.net/dtd/tasks-1.0.xsd
+    http://pear.php.net/dtd/package-2.0
+    http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Reflection</name>
+ <channel>pear.horde.org</channel>
+ <summary>Method reflection and documentation package</summary>
+ <description>The Reflection package provides methods for generationg code reflection and automatic documentation of methods.
+The package has a driver based output generation and currently generates HTML and Text_Wiki output.</description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jakob Munih</name>
+  <user>duck</user>
+  <email>duck@obala.net</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-02-14</date>
+ <time>13:32:45</time>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/lgpl.html">LGPL</license>
+ <notes>First release</notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Reflection">
+      <file name="Cli.php" role="php" />
+      <file name="Html.php" role="php" />
+      <file name="Wiki.php" role="php" />
+     </dir> <!-- /lib/Horde/Reflection -->
+     <file name="Reflection.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.0.3</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Reflection.php" as="Horde/Reflection.php" />
+   <install name="lib/Horde/Reflection/Cli.php" as="Horde/Reflection/Cli.php" />
+   <install name="lib/Horde/Reflection/Html.php" as="Horde/Reflection/Html.php" />
+   <install name="lib/Horde/Reflection/Wiki.php" as="Horde/Reflection/Wiki.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2008-02-14</date>
+   <license uri="http://www.gnu.org/licenses/lgpl.html">LGPL</license>
+   <notes>First release</notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/SQL/SQL.php b/framework/SQL/SQL.php
new file mode 100644 (file)
index 0000000..1522eb3
--- /dev/null
@@ -0,0 +1,437 @@
+<?php
+/**
+ * This is a utility class, every method is static.
+ *
+ * $Horde: framework/SQL/SQL.php,v 1.54 2009/09/25 07:53:26 selsky Exp $
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 2.2
+ * @package Horde_SQL
+ */
+class Horde_SQL {
+
+    /**
+     * Returns a boolean expression using the specified operator. Uses
+     * database-specific casting, if necessary.
+     *
+     * @param DB $dbh        The PEAR::DB database object.
+     * @param string $lhs    The column or expression to test.
+     * @param string $op     The operator.
+     * @param string $rhs    The comparison value.
+     * @param boolean $bind  If true, the method returns the query and a list
+     *                       of values suitable for binding as an array.
+     * @param array $params  Any additional parameters for the operator. @since
+     *                       Horde 3.2
+     *
+     * @return mixed  The SQL test fragment, or an array containing the query
+     *                and a list of values if $bind is true.
+     */
+    function buildClause(&$dbh, $lhs, $op, $rhs, $bind = false, $params = array())
+    {
+        switch ($op) {
+        case '|':
+        case '&':
+            switch ($dbh->phptype) {
+            case 'pgsql':
+                // Only PgSQL 7.3+ understands SQL99 'SIMILAR TO'; use
+                // ~ for greater backwards compatibility.
+                $query = 'CASE WHEN CAST(%s AS VARCHAR) ~ \'^-?[0-9]+$\' THEN (CAST(%s AS INTEGER) %s %s) <> 0 ELSE FALSE END';
+                if ($bind) {
+                    return array(sprintf(Horde_SQL::escapePrepare($query),
+                                         Horde_SQL::escapePrepare($lhs),
+                                         Horde_SQL::escapePrepare($lhs),
+                                         Horde_SQL::escapePrepare($op),
+                                         '?'),
+                                 array((int)$rhs));
+                } else {
+                    return sprintf($query, $lhs, $lhs, $op, (int)$rhs);
+                }
+
+            case 'oci8':
+                // Oracle uses & for variables. We need to use the bitand
+                // function that is available, but may be unsupported.
+                $query = 'bitand(%s, %s) = %s';
+                if ($bind) {
+                    return array(sprintf(Horde_SQL::escapePrepare($query),
+                                         Horde_SQL::escapePrepare($lhs), '?', '?'),
+                                 array((int)$rhs, (int)$rhs));
+                } else {
+                    return sprintf($query, $lhs, (int)$rhs, (int)$rhs);
+                }
+
+            case 'mssql':
+                // MSSQL must have a valid boolean expression
+                $query = '(CASE WHEN ISNUMERIC(%s) = 1 THEN (%s & %s) ELSE %s END) = %s';
+                if ($bind) {
+                    return array(sprintf(Horde_SQL::escapePrepare($query),
+                                         Horde_SQL::escapePrepare($lhs),
+                                         Horde_SQL::escapePrepare($lhs), '?', '?', '?'),
+                                 array((int)$rhs, (int)$rhs - 1, (int)$rhs));
+                } else {
+                    return sprintf($query, $lhs, $lhs, (int)$rhs, (int)$rhs - 1, (int)$rhs);
+                }
+
+            case 'odbc':
+                // ODBC must have a valid boolean expression
+                $query = '(%s & %s) = %s';
+                if ($bind) {
+                    return array(sprintf(Horde_SQL::escapePrepare($query),
+                                         Horde_SQL::escapePrepare($lhs), '?', '?'),
+                                 array((int)$rhs, (int)$rhs));
+                } else {
+                    return sprintf($query, $lhs, (int)$rhs, (int)$rhs);
+                }
+
+            default:
+                if ($bind) {
+                    return array($lhs . ' ' . Horde_SQL::escapePrepare($op) . ' ?',
+                                 array((int)$rhs));
+                } else {
+                    return $lhs . ' ' . $op . ' ' . (int)$rhs;
+                }
+            }
+
+        case '~':
+            if ($dbh->phptype == 'mysql') {
+                $op = 'REGEXP';
+            }
+            if ($bind) {
+                return array($lhs . ' ' . $op . ' ?', array($rhs));
+            } else {
+                return $lhs . ' ' . $op . ' ' . $rhs;
+            }
+
+        case 'IN':
+            if ($bind) {
+                if (is_array($rhs)) {
+                    return array($lhs . ' IN (?' . str_repeat(', ?', count($rhs) - 1) . ')', $rhs);
+                } else {
+                    // We need to bind each member of the IN clause
+                    // separately to ensure proper quoting.
+                    if (substr($rhs, 0, 1) == '(') {
+                        $rhs = substr($rhs, 1);
+                    }
+                    if (substr($rhs, -1) == ')') {
+                        $rhs = substr($rhs, 0, -1);
+                    }
+
+                    $ids = preg_split('/\s*,\s*/', $rhs);
+
+                    return array($lhs . ' IN (?' . str_repeat(', ?', count($ids) - 1) . ')', $ids);
+                }
+            } else {
+                if (is_array($rhs)) {
+                    return $lhs . ' IN ' . implode(', ', $rhs);
+                } else {
+                    return $lhs . ' IN ' . $rhs;
+                }
+            }
+
+        case 'LIKE':
+            if ($dbh->phptype == 'pgsql') {
+                $query = '%s ILIKE %s';
+            } else {
+                $query = 'LOWER(%s) LIKE LOWER(%s)';
+            }
+            if ($bind) {
+                if (empty($params['begin'])) {
+                    return array(sprintf($query,
+                                         Horde_SQL::escapePrepare($lhs),
+                                         '?'),
+                                 array('%' . $rhs . '%'));
+                } else {
+                    return array(sprintf('(' . $query . ' OR ' . $query . ')',
+                                         Horde_SQL::escapePrepare($lhs),
+                                         '?',
+                                         Horde_SQL::escapePrepare($lhs),
+                                         '?'),
+                                 array($rhs . '%', '% ' . $rhs . '%'));
+                }
+            } else {
+                if (empty($params['begin'])) {
+                    return sprintf($query,
+                                   $lhs,
+                                   $dbh->quote('%' . $rhs . '%'));
+                } else {
+                    return sprintf('(' . $query . ' OR ' . $query . ')',
+                                   $lhs,
+                                   $dbh->quote($rhs . '%'),
+                                   $lhs,
+                                   $dbh->quote('% ' . $rhs . '%'));
+                }
+            }
+
+        default:
+            if ($bind) {
+                return array($lhs . ' ' . Horde_SQL::escapePrepare($op) . ' ?', array($rhs));
+            } else {
+                return $lhs . ' ' . $op . ' ' . $dbh->quote($rhs);
+            }
+        }
+    }
+
+    /**
+     * Escapes all characters in a string that are placeholders for the
+     * prepare/execute methods of the DB package.
+     *
+     * @param string $query  A string to escape.
+     *
+     * @return string  The correctly escaped string.
+     */
+    function escapePrepare($query)
+    {
+        return preg_replace('/[?!&]/', '\\\\$0', $query);
+    }
+
+    function readBlob(&$dbh, $table, $field, $criteria)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('You must specify the fetch criteria');
+        }
+
+        $where = '';
+
+        switch ($dbh->dbsyntax) {
+        case 'oci8':
+            foreach ($criteria as $key => $value) {
+                if (!empty($where)) {
+                    $where .= ' AND ';
+                }
+                if (empty($value)) {
+                    $where .= $key . ' IS NULL';
+                } else {
+                    $where .= $key . ' = ' . $dbh->quote($value);
+                }
+            }
+
+            $statement = OCIParse($dbh->connection,
+                                  sprintf('SELECT %s FROM %s WHERE %s',
+                                          $field, $table, $where));
+            OCIExecute($statement);
+            if (OCIFetchInto($statement, $lob)) {
+                $result = $lob[0]->load();
+            } else {
+                $result = PEAR::raiseError('Unable to load SQL Data');
+            }
+            OCIFreeStatement($statement);
+            break;
+
+        default:
+            foreach ($criteria as $key => $value) {
+                if (!empty($where)) {
+                    $where .= ' AND ';
+                }
+                $where .= $key . ' = ' . $dbh->quote($value);
+            }
+            $result = $dbh->getOne(sprintf('SELECT %s FROM %s WHERE %s',
+                                           $field, $table, $where));
+
+            switch ($dbh->dbsyntax) {
+            case 'mssql':
+            case 'pgsql':
+                $result = pack('H' . strlen($result), $result);
+                break;
+            }
+        }
+
+        return $result;
+    }
+
+    function insertBlob(&$dbh, $table, $field, $data, $attributes)
+    {
+        $fields = array();
+        $values = array();
+
+        switch ($dbh->dbsyntax) {
+        case 'oci8':
+            foreach ($attributes as $key => $value) {
+                $fields[] = $key;
+                $values[] = $dbh->quote($value);
+            }
+
+            $statement = OCIParse($dbh->connection,
+                                  sprintf('INSERT INTO %s (%s, %s)' .
+                                          ' VALUES (%s, EMPTY_BLOB()) RETURNING %s INTO :blob',
+                                          $table,
+                                          implode(', ', $fields),
+                                          $field,
+                                          implode(', ', $values),
+                                          $field));
+
+            $lob = OCINewDescriptor($dbh->connection);
+            OCIBindByName($statement, ':blob', $lob, -1, SQLT_BLOB);
+            OCIExecute($statement, OCI_DEFAULT);
+            $lob->save($data);
+            $result = OCICommit($dbh->connection);
+            $lob->free();
+            OCIFreeStatement($statement);
+            return $result ? true : PEAR::raiseError('Unknown Error');
+
+        default:
+            foreach ($attributes as $key => $value) {
+                $fields[] = $key;
+                $values[] = $value;
+            }
+
+            $query = sprintf('INSERT INTO %s (%s, %s) VALUES (%s)',
+                             $table,
+                             implode(', ', $fields),
+                             $field,
+                             '?' . str_repeat(', ?', count($values)));
+            break;
+        }
+
+        switch ($dbh->dbsyntax) {
+        case 'mssql':
+        case 'pgsql':
+            $values[] = bin2hex($data);
+            break;
+
+        default:
+            $values[] = $data;
+        }
+
+        /* Log the query at a DEBUG log level. */
+        Horde::logMessage(sprintf('SQL Query by Horde_SQL::insertBlob(): query = "%s"', $query),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Execute the query. */
+        return $this->_db->query($query, $values);
+    }
+
+    function updateBlob(&$dbh, $table, $field, $data, $where, $alsoupdate)
+    {
+        $fields = array();
+        $values = array();
+
+        switch ($dbh->dbsyntax) {
+        case 'oci8':
+            $wherestring = '';
+            foreach ($where as $key => $value) {
+                if (!empty($wherestring)) {
+                    $wherestring .= ' AND ';
+                }
+                $wherestring .= $key . ' = ' . $dbh->quote($value);
+            }
+
+            $statement = OCIParse($dbh->connection,
+                                  sprintf('SELECT %s FROM %s WHERE %s FOR UPDATE',
+                                          $field,
+                                          $table,
+                                          $wherestring));
+
+            OCIExecute($statement, OCI_DEFAULT);
+            OCIFetchInto($statement, $lob);
+            $lob[0]->save($data);
+            $result = OCICommit($dbh->connection);
+            $lob[0]->free();
+            OCIFreeStatement($statement);
+            return $result ? true : PEAR::raiseError('Unknown Error');
+
+        default:
+            $updatestring = '';
+            $values = array();
+            foreach ($alsoupdate as $key => $value) {
+                $updatestring .= $key . ' = ?, ';
+                $values[] = $value;
+            }
+            $updatestring .= $field . ' = ?';
+            switch ($dbh->dbsyntax) {
+            case 'mssql':
+            case 'pgsql':
+                $values[] = bin2hex($data);
+                break;
+
+            default:
+                $values[] = $data;
+            }
+
+            $wherestring = '';
+            foreach ($where as $key => $value) {
+                if (!empty($wherestring)) {
+                    $wherestring .= ' AND ';
+                }
+                $wherestring .= $key . ' = ?';
+                $values[] = $value;
+            }
+
+            $query = sprintf('UPDATE %s SET %s WHERE %s',
+                             $table,
+                             $updatestring,
+                             $wherestring);
+            break;
+        }
+
+        /* Log the query at a DEBUG log level. */
+        Horde::logMessage(sprintf('SQL Query by Horde_SQL::updateBlob(): query = "%s"', $query),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Execute the query. */
+        return $dbh->query($query, $values);
+    }
+
+    /**
+     * Build an SQL SET clause.
+     *
+     * This function takes an array in the form column => value and returns
+     * an SQL SET clause (without the SET keyword) with the values properly
+     * quoted.  For example, the following:
+     *
+     *      array('foo' => 1,
+     *            'bar' => 'hello')
+     *
+     * would result in the fragment:
+     *
+     *      foo = 1, bar = 'hello'
+     *
+     * @param DB $dbh        The PEAR::DB database object.
+     * @param array $values  The array of column => value pairs.
+     *
+     * @return string  The SQL SET fragment.
+     */
+    function updateValues(&$dbh, $values)
+    {
+        $ret = array();
+        foreach ($values as $key => $value) {
+            $ret[] = $key . ' = ' . ($value === null ? 'NULL' : $dbh->quote($value));
+        }
+        return implode(', ', $ret);
+    }
+
+    /**
+     * Build an SQL INSERT/VALUES clause.
+     *
+     * This function takes an array in the form column => value and returns
+     * an SQL fragment specifying the column names and insert values, with
+     * the values properly quoted.  For example, the following:
+     *
+     *      array('foo' => 1,
+     *            'bar' => 'hello')
+     *
+     * would result in the fragment:
+     *
+     *      ( foo, bar ) VALUES ( 1, 'hello' )
+     *
+     * @param DB $dbh        The PEAR::DB database object.
+     * @param array $values  The array of column => value pairs.
+     *
+     * @return string  The SQL fragment.
+     */
+    function insertValues(&$dbh, $values)
+    {
+        $columns = array();
+        $vals = array();
+        foreach ($values as $key => $value) {
+            $columns[] = $key;
+            $vals[] = $value === null ? 'NULL' : $dbh->quote($value);
+        }
+        return '( ' . implode(', ', $columns) . ' ) VALUES ( ' . implode(', ', $vals) . ' )';
+    }
+
+}
diff --git a/framework/SQL/SQL/Keywords.php b/framework/SQL/SQL/Keywords.php
new file mode 100644 (file)
index 0000000..64c76da
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+/**
+ * This class provides a parser which can construct an SQL WHERE
+ * clause from a Google-like search expression.
+ *
+ * $Horde: framework/SQL/SQL/Keywords.php,v 1.12 2009/01/06 17:49:42 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * The expression recognizes boolean "AND", "OR", and "NOT" (providing
+ * no operator between keywords implies "AND"), like so:
+ *
+ *   cat and dog
+ *   cat or dog
+ *   cat and not dog
+ *
+ * If no operator appears between keywords or quoted strings, "AND" is
+ * assumed.  A comma can be used instead of "OR":
+ *
+ *   cat dog
+ *   cat, dog
+ *   cat not dog
+ *
+ * The parser recognizes parentheses, so complex expressions can be
+ * created:
+ *
+ *   cat and not (dog or puppy)
+ *
+ * Quoted strings are also recognized, and are taken as literal
+ * keywords:
+ *
+ *   "cat and dog"
+ *
+ * Parsing is designed to be as fuzzy as possible, so it shouldn't
+ * error unless people search for "AND", "OR", or "NOT" without
+ * quoting it or use unbalanced parentheses.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @since   Horde 3.0
+ * @package Horde_SQL
+ */
+class Horde_SQL_Keywords {
+
+    /**
+     * Parse a keyword expression.
+     *
+     * @param string $column        This is the SQL field name the resulting
+     *                              expression should test against.
+     * @param string $expr          This is the keyword expression we want to
+     *                              parse.
+     * @return mixed the query expression or a PEAR_Error on failure.
+     */
+    function parse($column, $expr)
+    {
+        /* First pass - scan the string for tokens.  Bare words are
+         * tokens, or the user can quote strings to have embedded
+         * spaces, keywords, or parentheses.  Parentheses can be used
+         * for grouping boolean operators, and the boolean operators
+         * AND, OR, and NOT are all recognized.
+         *
+         * The tokens are returned in the $tokens array -- an array of
+         * strings.  Each string in the array starts with either a `!'
+         * or a `='.  `=' is a bare word or quoted string we are
+         * searching for, and `!' indicates a boolean operator or
+         * parenthesis.  A token that starts with a '.'  indicates a
+         * PostgreSQL word boundary search. */
+        $tokens = array();
+        while (!empty($expr)) {
+            $expr = preg_replace("/^\s+/", "", $expr);
+            if (empty($expr)) {
+                break;
+            }
+            if (substr($expr,0,1) == '(') {
+                $expr = substr($expr, 1);
+                $token = '!(';
+            } elseif (substr($expr, 0, 1) == ')') {
+                $expr = substr($expr, 1);
+                $token = '!)';
+            } elseif (substr($expr, 0, 1) == ',') {
+                $expr = substr($expr, 1);
+                $token = '!OR';
+            } elseif (preg_match("/^(AND|OR|NOT)([^a-z].*)?$/i", $expr,
+                                 $matches)) {
+                $token = '!' . strtoupper($matches[1]);
+                $expr = substr($expr, strlen($matches[1]));
+            } elseif (preg_match("/^\"(([^\"]|\\[0-7]+|\\[Xx][0-9a-fA-F]+|\\[^Xx0-7])*)\"/",
+                                 $expr, $matches)) {
+                $token = '=' . stripcslashes($matches[1]);
+                $expr = substr($expr, strlen($matches[0]));
+            } elseif (preg_match("/^[^\\s\\(\\),]+/", $expr, $matches)) {
+                $token = '=' . $matches[0];
+                $expr = substr($expr,strlen($token)-1);
+            } else {
+                return PEAR::raiseError('Syntax error in search terms');
+            }
+            if ($token == '!AND') {
+                /* !AND is implied by concatenation. */
+                continue;
+            }
+            $tokens[] = $token;
+        }
+
+        /* Call the expression parser. */
+        return Horde_SQL_Keywords::_parseKeywords1($column, $tokens);
+    }
+
+    function _parseKeywords1($column, &$tokens)
+    {
+        if (count($tokens) == 0) {
+            return PEAR::raiseError('Empty search terms');
+        }
+        $lhs = Horde_SQL_Keywords::_parseKeywords2($column, $tokens);
+        if (is_a($lhs, 'PEAR_Error')) {
+            return $lhs;
+        }
+        if (count($tokens) == 0 || $tokens[0] != '!OR') {
+            return $lhs;
+        }
+        array_shift($tokens);
+        $rhs = Horde_SQL_Keywords::_parseKeywords1($column, $tokens);
+        if (is_a($rhs, 'PEAR_Error')) {
+            return $rhs;
+        }
+        return "($lhs OR $rhs)";
+    }
+
+    function _parseKeywords2($column, &$tokens)
+    {
+        $lhs = Horde_SQL_Keywords::_parseKeywords3($column, $tokens);
+        if (is_a($lhs, 'PEAR_Error')) {
+            return $lhs;
+        }
+        if (sizeof($tokens) == 0 || $tokens[0] == '!)' || $tokens[0] == '!OR') {
+            return $lhs;
+        }
+        $rhs = Horde_SQL_Keywords::_parseKeywords2($column, $tokens);
+        if (is_a($rhs, 'PEAR_Error')) {
+            return $rhs;
+        }
+        return "($lhs AND $rhs)";
+    }
+
+    function _parseKeywords3($column, &$tokens)
+    {
+        if ($tokens[0] == '!NOT') {
+            array_shift($tokens);
+            $lhs = Horde_SQL_Keywords::_parseKeywords4($column, $tokens);
+            if (is_a($lhs, 'PEAR_Error')) {
+                return $lhs;
+            }
+            return "(NOT $lhs)";
+        }
+        return Horde_SQL_Keywords::_parseKeywords4($column, $tokens);
+    }
+
+    function _parseKeywords4($column, &$tokens)
+    {
+        if ($tokens[0] == '!(') {
+            array_shift($tokens);
+            $lhs = Horde_SQL_Keywords::_parseKeywords1($column, $tokens);
+            if (is_a($lhs, 'PEAR_Error')) {
+                return $lhs;
+            }
+            if (sizeof($tokens) == 0 || $tokens[0] != '!)') {
+                return PEAR::raiseError('Expected ")"');
+            }
+            array_shift($tokens);
+            return $lhs;
+        }
+
+        if (substr($tokens[0], 0, 1) != '=' &&
+            substr($tokens[0], 0, 2) != '=.') {
+            return PEAR::raiseError('Expected bare word or quoted search term');
+        }
+
+        $val = strtolower(substr(array_shift($tokens), 1));
+        $val = addslashes(ereg_replace("([\\%])", "\\\\1", $val));
+
+        return "(LOWER($column) LIKE '%$val%')";
+    }
+
+}
diff --git a/framework/SQL/SQL/Manager.php b/framework/SQL/SQL/Manager.php
new file mode 100644 (file)
index 0000000..2d34a77
--- /dev/null
@@ -0,0 +1,340 @@
+<?php
+/**
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Db
+ */
+
+/** MDB2_Schema */
+require_once 'MDB2/Schema.php';
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Db
+ */
+class Horde_SQL_Manager
+{
+    /**
+     * Database manager for write operations
+     * @var MDB2_Schema
+     */
+    var $_writer;
+
+    /**
+     * Database manager for read operations
+     * @var MDB2_Schema
+     */
+    var $_reader;
+
+    /**
+     * Create a new schema manager.
+     *
+     * @param array $dsn  Overrides global Horde SQL config.
+     */
+    function getInstance($dsn = array())
+    {
+        // Merge local options with Horde database config.
+        if (isset($GLOBALS['conf']['sql'])) {
+            $dsn = array_merge($GLOBALS['conf']['sql'], $dsn);
+        }
+        unset($dsn['charset']);
+        $options = array('seqcol_name' => 'id',
+                         'portability' => MDB2_PORTABILITY_ALL & ~MDB2_PORTABILITY_FIX_CASE,
+                         'force_defaults' => false);
+
+        $writer = MDB2_Schema::factory($dsn, $options);
+        if (is_a($writer, 'PEAR_Error')) {
+            return $writer;
+        }
+
+        // Check if we need to set up the read DB connection seperately.
+        $reader = null;
+        if (!empty($dsn['splitread'])) {
+            $read_dsn = array_merge($dsn, $dsn['read']);
+            unset($read_dsn['charset']);
+
+            $reader = MDB2_Schema::factory($read_dsn, $options);
+            if (is_a($reader, 'PEAR_Error')) {
+                return $reader;
+            }
+        }
+
+        return new Horde_SQL_Manager($writer, $reader);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param MDB2_Schema $writer DB manager for the write database.
+     * @param MDB2_Schema $reader DB manager for the read database (defaults to using $writer).
+     */
+    function Horde_SQL_Manager($writer, $reader = null)
+    {
+        $this->_writer = $writer;
+        if ($reader !== null) {
+            $this->_reader = $reader;
+        } else {
+            $this->_reader = $writer;
+        }
+    }
+
+    /**
+     * Dump XML schema info for $tables
+     *
+     * @param array $tables Tables to get XML for
+     *
+     * @return string XML schema
+     */
+    function dumpSchema($tables = array())
+    {
+        $defs = $this->getTableDefinitions($tables);
+        if (is_a($defs, 'PEAR_Error')) {
+            return $defs;
+        }
+
+        // Make the database name a variable
+        $defs['name'] = '<variable>name</variable>';
+
+        $args = array(
+            'output_mode' => 'function',
+            'output' => array(&$this, '_collectXml'),
+        );
+        $this->_xml = '';
+        $this->_reader->dumpDatabase($defs, $args, MDB2_SCHEMA_DUMP_STRUCTURE);
+        $xml = $this->_xml;
+        $this->_xml = '';
+        return $xml;
+    }
+
+    /**
+     * Dump XML data for $tables
+     *
+     * @param array $tables Tables to dump data for.
+     *
+     * @return string XML data
+     */
+    function dumpData($outfile, $tables = array())
+    {
+        $defs = $this->getTableDefinitions($tables);
+        if (is_a($defs, 'PEAR_Error')) {
+            return $defs;
+        }
+
+        // Make the database name a variable
+        $defs['name'] = '<variable>name</variable>';
+
+        $args = array(
+            'output_mode' => 'file',
+            'output' => $outfile,
+        );
+        return $this->_reader->dumpDatabase($defs, $args, MDB2_SCHEMA_DUMP_CONTENT);
+    }
+
+    /**
+     * Update the database using an XML schema file
+     *
+     * @param string $schema_file  The local filename of a .xml schema file.
+     * @param boolean $debug       Whether to return the SQL statements instead of
+     *                             doing the upgrade.
+     *
+     * @return
+     */
+    function updateSchema($schema_file, $debug = false)
+    {
+        if (!file_exists($schema_file) || !is_readable($schema_file)) {
+            return PEAR::raiseError('Unable to read ' . $schema_file);
+        }
+
+        $existing = $this->getTableDefinitions();
+        if (is_a($existing, 'PEAR_Error')) {
+            return $existing;
+        }
+
+        if ($debug) {
+            $this->_writer->db->setOption('debug', true);
+            $this->_writer->db->setOption('debug_handler', 'MDB2_defaultDebugOutput');
+        }
+
+        $result = $this->_writer->updateDatabase(
+            $schema_file,
+            $existing,
+            array('name' => $this->_writer->db->database_name,
+                  'create' => false),
+            $debug);
+
+        return $debug ? $this->_writer->db->getDebugOutput() : $result;
+    }
+
+    /**
+     * Update the database using an XML schema file
+     *
+     * @param string $data_file    The local filename of a .xml data file.
+     *
+     * @return
+     */
+    function updateData($data_file)
+    {
+        if (!file_exists($data_file) || !is_readable($data_file)) {
+            return PEAR::raiseError('Unable to read ' . $data_file);
+        }
+
+        $schema = $this->getTableDefinitions();
+        if (is_a($schema, 'PEAR_Error')) {
+            return $schema;
+        }
+
+        return $this->_writer->writeInitialization(
+            $data_file,
+            $schema,
+            array('name' => $this->_writer->db->database_name));
+    }
+
+    /**
+     * Wraps MDB2_Schema code to avoid overly strict validation and to
+     * allow dumping a selective table list.
+     *
+     * @param array $tables Tables to get definitions for. If empty, all tables are dumped.
+     */
+    function getTableDefinitions($tables = array())
+    {
+        if (!count($tables)) {
+            $tables = $this->_reader->db->manager->listTables();
+            if (PEAR::isError($tables)) {
+                return $tables;
+            }
+        }
+
+        $database_definition = array(
+            'name' => '',
+            'create' => false,
+            'overwrite' => false,
+            'charset' => '',
+            'description' => '',
+            'comments' => '',
+            'tables' => array(),
+            'sequences' => array(),
+        );
+
+        foreach ($tables as $table_name) {
+            $fields = $this->_reader->db->manager->listTableFields($table_name);
+            if (PEAR::isError($fields)) {
+                return $fields;
+            }
+
+            $database_definition['tables'][$table_name] = array(
+                'was' => '',
+                'description' => '',
+                'comments' => '',
+                'fields' => array(),
+                'indexes' => array(),
+                'constraints' => array(),
+                'initialization' => array()
+            );
+
+            $table_definition =& $database_definition['tables'][$table_name];
+            foreach ($fields as $field_name) {
+                $definition = $this->_reader->db->reverse->getTableFieldDefinition($table_name, $field_name);
+                if (PEAR::isError($definition)) {
+                    return $definition;
+                }
+
+                if (!empty($definition[0]['autoincrement'])) {
+                    $definition[0]['default'] = '0';
+                }
+                $table_definition['fields'][$field_name] = $definition[0];
+                $field_choices = count($definition);
+                if ($field_choices > 1) {
+                    $warning = "There are $field_choices type choices in the table $table_name field $field_name (#1 is the default): ";
+                    $field_choice_cnt = 1;
+                    $table_definition['fields'][$field_name]['choices'] = array();
+                    foreach ($definition as $field_choice) {
+                        $table_definition['fields'][$field_name]['choices'][] = $field_choice;
+                        $warning.= 'choice #'.($field_choice_cnt).': '.serialize($field_choice);
+                        $field_choice_cnt++;
+                    }
+                    $this->_reader->warnings[] = $warning;
+                }
+            }
+
+            $keys = array();
+            $indexes = $this->_reader->db->manager->listTableIndexes($table_name);
+            if (PEAR::isError($indexes)) {
+                return $indexes;
+            }
+
+            if (is_array($indexes)) {
+                foreach ($indexes as $index_name) {
+                    $this->_reader->db->expectError(MDB2_ERROR_NOT_FOUND);
+                    $definition = $this->_reader->db->reverse->getTableIndexDefinition($table_name, $index_name);
+                    $this->_reader->db->popExpect();
+                    if (PEAR::isError($definition)) {
+                        if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) {
+                            continue;
+                        }
+                        return $definition;
+                    }
+
+                    $keys[$index_name] = $definition;
+                }
+            }
+
+            $constraints = $this->_reader->db->manager->listTableConstraints($table_name);
+            if (PEAR::isError($constraints)) {
+                return $constraints;
+            }
+
+            if (is_array($constraints)) {
+                foreach ($constraints as $constraint_name) {
+                    $this->_reader->db->expectError(MDB2_ERROR_NOT_FOUND);
+                    $definition = $this->_reader->db->reverse->getTableConstraintDefinition($table_name, $constraint_name);
+                    $this->_reader->db->popExpect();
+                    if (PEAR::isError($definition)) {
+                        if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) {
+                            continue;
+                        }
+                        return $definition;
+                    }
+
+                    $keys[$constraint_name] = $definition;
+                }
+            }
+
+            foreach ($keys as $key_name => $definition) {
+                if (array_key_exists('foreign', $definition) && $definition['foreign']) {
+                    foreach ($definition['fields'] as $field_name => $field) {
+                        $definition['fields'][$field_name] = '';
+                    }
+
+                    foreach ($definition['references']['fields'] as $field_name => $field) {
+                        $definition['references']['fields'][$field_name] = '';
+                    }
+
+                    $table_definition['constraints'][$key_name] = $definition;
+                } else {
+                    foreach ($definition['fields'] as $field_name => $field) {
+                        $definition['fields'][$field_name] = $field;
+                    }
+
+                    $table_definition['indexes'][$key_name] = $definition;
+                }
+            }
+        }
+
+        return $database_definition;
+    }
+
+    /**
+     * Scheme dumping callback for MDB2_Schema_Writer
+     * @deprecated
+     */
+    function _collectXml($xml)
+    {
+        $this->_xml .= $xml;
+    }
+
+}
diff --git a/framework/SQL/package.xml b/framework/SQL/package.xml
new file mode 100644 (file)
index 0000000..990b00f
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_SQL</name>
+ <channel>pear.horde.org</channel>
+ <summary>SQL Utility Class</summary>
+ <description>Horde_SQL:: contains some utility functions for dealing with SQL.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>23:26:59</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="SQL">
+    <file baseinstalldir="/Horde" name="Keywords.php" role="php" />
+    <file baseinstalldir="/Horde" name="Manager.php" role="php" />
+   </dir> <!-- //SQL -->
+   <file baseinstalldir="/Horde" name="SQL.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Scheduler/lib/Horde/Scheduler.php b/framework/Scheduler/lib/Horde/Scheduler.php
new file mode 100644 (file)
index 0000000..283a1e6
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+
+require_once 'Horde.php';
+require_once 'VFS.php';
+
+/**
+ * Horde_Scheduler
+ *
+ * $Horde: framework/Scheduler/lib/Horde/Scheduler.php,v 1.3 2009/07/14 00:25:30 mrubinsk Exp $
+ *
+ * @package Horde_Scheduler
+ */
+class Horde_Scheduler {
+
+    /**
+     * Name of the sleep function.
+     *
+     * @var string
+     */
+    var $_sleep;
+
+    /**
+     * Adjustment factor to sleep in microseconds.
+     *
+     * @var integer
+     */
+    var $_sleep_adj;
+
+    /**
+     * Constructor.
+     *
+     * Figures out how we can best sleep with microsecond precision
+     * based on what platform we're running on.
+     */
+    function Horde_Scheduler()
+    {
+        if (!strncasecmp(PHP_OS, 'WIN', 3)) {
+            $this->_sleep = 'sleep';
+            $this->_sleep_adj = 1000000;
+        } else {
+            $this->_sleep = 'usleep';
+            $this->_sleep_adj = 1;
+        }
+    }
+
+    /**
+     * Main loop/action function.
+     *
+     * @abstract
+     */
+    function run()
+    {
+    }
+
+    /**
+     * Preserve the internal state of the scheduler object that we are
+     * passed, and save it to the Horde VFS backend. Horde_Scheduler
+     * objects should define __sleep() and __wakeup() serialization
+     * callbacks for anything that needs to be done at object
+     * serialization or deserialization - handling database
+     * connections, etc.
+     *
+     * @param string  $id  An id to uniquely identify this scheduler from
+     *                     others of the same class.
+     */
+    function serialize($id = '')
+    {
+        $vfs = &VFS::singleton($GLOBALS['conf']['vfs']['type'],
+                               Horde::getDriverConfig('vfs', $GLOBALS['conf']['vfs']['type']));
+        if (is_a($vfs, 'PEAR_Error')) {
+            Horde::logMessage($vfs, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+
+        $result = $vfs->writeData('.horde/scheduler', Horde_String::lower(get_class($this)) . $id, serialize($this), true);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Restore a Horde_Scheduler object from the cache.
+     *
+     * @param string  $class     The name of the Horde_Scheduler object to restore.
+     * @param string  $id        An id to uniquely identify this
+     *                           scheduler from others of the same class.
+     * @param boolean $autosave  Automatically store (serialize) the returned
+     *                           object at script shutdown.
+     *
+     * @see Horde_Scheduler::serialize()
+     */
+    function &unserialize($class, $id = '', $autosave = true)
+    {
+        // Need a lowercase version of the classname, and a default
+        // instance of the scheduler object in case we can't retrieve
+        // one.
+        $class = strtolower($class);
+        $scheduler = new $class;
+
+        $vfs = &VFS::singleton($GLOBALS['conf']['vfs']['type'],
+                               Horde::getDriverConfig('vfs', $GLOBALS['conf']['vfs']['type']));
+        if (is_a($vfs, 'PEAR_Error')) {
+            Horde::logMessage($vfs, __FILE__, __LINE__, PEAR_LOG_ERR);
+        } else {
+            $data = $vfs->read('.horde/scheduler', $class . $id);
+            if (is_a($data, 'PEAR_Error')) {
+                Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_INFO);
+            } else {
+                $scheduler = @unserialize($data);
+                if (!$scheduler) {
+                    $scheduler = new $class;
+                }
+            }
+        }
+
+        if ($autosave) {
+            register_shutdown_function(array(&$scheduler, 'serialize'));
+        }
+
+        return $scheduler;
+    }
+
+    /**
+     * Platform-independant sleep $msec microseconds.
+     *
+     * @param integer $msec  Microseconds to sleep.
+     */
+    function sleep($msec)
+    {
+        call_user_func($this->_sleep, $msec / $this->_sleep_adj);
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Scheduler instance based on $driver.
+     *
+     * @param string $driver The type of concrete Horde_Scheduler subclass to
+     *                       return.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return Horde_Scheduler  The newly created concrete Horde_Scheduler
+     *                          instance, or an error object.
+     */
+    function factory($driver, $params = null)
+    {
+        $driver = basename($driver);
+        $class = 'Horde_Scheduler_' . $driver;
+        if (!class_exists($class)) {
+            include 'Horde/Scheduler/' . $driver . '.php';
+        }
+
+        if (class_exists($class)) {
+            return new $class($params);
+        } else {
+            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+    }
+
+}
diff --git a/framework/Scheduler/lib/Horde/Scheduler/cron.php b/framework/Scheduler/lib/Horde/Scheduler/cron.php
new file mode 100644 (file)
index 0000000..15eb8df
--- /dev/null
@@ -0,0 +1,419 @@
+<?php
+/**
+ * Horde_Scheduler_cron:: Sort of a cron replacement in a PHP cli
+ * script.
+ *
+ * Date Syntax Examples.
+ *
+ * Remember:
+ *   - Whitespace (space, tab, newline) delimited fields
+ *   - Single values, sets, ranges, wildcards
+ *
+ * SECOND   MINUTE              HOUR        DAY     MONTH
+ * *        *                   *           *       *       (every second)
+ * 0,30     *                   *           *       *       (every 30 seconds)
+ * 0        0,10,20,30,40,50    *           *       *       (every 10 minutes)
+ * 0        0                   *           *       *       (beginning of every hour)
+ * 0        0                   0,6,12,18   *       *       (at midnight, 6am, noon, 6pm)
+ * 0        0                   0           1-7&Fri *       (midnight, first Fri of the month)
+ * 0        0                   0           1-7!Fri *       (midnight, first Mon-Thu,Sat-Sun of the month)
+ *
+ *
+ * Example usage:
+ *
+ * @set_time_limit(0);
+ * require_once 'Horde/Scheduler.php';
+ * $cron = Horde_Scheduler::factory('cron');
+ *
+ * // Run this command every 5 minutes.
+ * $cron->addTask('perl somescript.pl', '0 0,5,10,15,20,25,30,35,40,45,50,55 * * *');
+ *
+ * // Run this command midnight of the first Friday of odd numbered months.
+ * $cron->addTask('php -q somescript.php', '0 0 0 1-7&Fri 1,3,5,7,9,11');
+ *
+ * // Also run this command midnight of the second Thursday and Saturday of the even numbered months.
+ * $cron->addTask('php -q somescript.php', '0 0 0 8-15&Thu,8-15&Sat 2,4,6,8,10,12');
+ *
+ * $cron->run();
+ *
+ * $Horde: framework/Scheduler/lib/Horde/Scheduler/cron.php,v 1.3 2009/07/14 00:25:30 mrubinsk Exp $
+ *
+ * @author  Ryan Flynn <ryan@ryanflynn.com>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Horde_Scheduler
+ */
+class Horde_Scheduler_cron extends Horde_Scheduler {
+
+    var $_tasks = array();
+
+    /**
+     * Every time a task is added it will get a fresh uid even if
+     * immediately removed.
+     */
+    var $_counter = 1;
+
+    function addTask($cmd, $rules)
+    {
+        $ds = new Horde_Scheduler_cronDate($rules);
+
+        $this->_counter++;
+
+        $this->_tasks[] =
+            array(
+                'uid' => $this->_counter,
+                'rules' => $ds,
+                'cmd' => $cmd
+            );
+
+        return $this->_counter;
+    }
+
+    function removeTask($uid)
+    {
+        $count = count($this->_tasks);
+        for ($i = 0; $i < $count; $i++) {
+            if ($this->_tasks['uid'] == $uid) {
+                $found = $i;
+                array_splice($this->_tasks, $i);
+                return $i;
+            }
+        }
+
+        return 0;
+    }
+
+    function run()
+    {
+        if (!count($this->_tasks)) {
+            exit("crond: Nothing to schedule; exiting.\n");
+        }
+
+        while (true) {
+            $t = time();
+
+            // Check each task.
+            foreach ($this->_tasks as $task) {
+                if ($task['rules']->nowMatches()) {
+                    $this->runcmd($task);
+                }
+            }
+
+            // Wait until the next second.
+            while (time() == $t) {
+                $this->sleep(100000);
+            }
+        }
+    }
+
+    function runcmd(&$task)
+    {
+        Horde::logMessage('Horde_Scheduler_Cron::runcmd(): ' . $task['cmd'] . ' run by ' . $task['uid'], __FILE__, __LINE__, PEAR_LOG_INFO);
+        return shell_exec($task['cmd']);
+    }
+
+}
+
+/**
+ * @package Horde_Scheduler
+ */
+class Horde_Scheduler_cronDate {
+
+    var $legalDays = array('MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN');
+
+    var $sec;
+    var $min;
+    var $hour;
+    var $day;
+    var $month;
+
+    function Horde_Scheduler_cronDate($raw)
+    {
+        $this->parse(Horde_String::upper($raw));
+    }
+
+    function nowMatches()
+    {
+        return $this->scheduledAt(time());
+    }
+
+    function scheduledAt($ts = null)
+    {
+        if ($ts === null) {
+            $ts = time();
+        }
+        return ($this->monthMatches($ts) &&
+                $this->monthMatches($ts) &&
+                $this->dayMatches($ts) &&
+                $this->hourMatches($ts) &&
+                $this->minMatches($ts) &&
+                $this->secMatches($ts));
+    }
+
+    function monthMatches($ts)
+    {
+        if ($this->month == '*') {
+            return true;
+        }
+
+        $currentmonth = '-' . date('n', $ts) . '-';
+
+        return (bool)strpos($this->month, $currentmonth);
+    }
+
+    function dayMatches($ts)
+    {
+        if (!empty($this->day['value']) && $this->day['value'] == '*') {
+            return true;
+        }
+
+        $currentdaynum = '-' . date('j', $ts) . '-';
+        $currentdaytxt = Horde_String::upper(date('D'));
+
+        foreach ($this->day as $day) {
+            if (@strpos($day['not'], $currentdaytxt) === false) {
+                $v1 = (@strpos($day['value'], $currentdaynum) !== false);
+                $v2 = (@strpos($day['and'], $currentdaytxt) !== false);
+
+                if (!empty($day['and']) && ($v1 && $v2)) {
+                    return true;
+                } elseif (empty($day['and']) && $v1) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    function hourMatches($ts)
+    {
+        if ($this->hour == '*') {
+            return true;
+        }
+
+        $currenthour = '-' . date('G', $ts) . '-';
+
+        return (strpos($this->hour, $currenthour) !== false);
+    }
+
+    function minMatches($ts)
+    {
+        if ($this->min == '*') {
+            return true;
+        }
+
+        $currentmin = '-' . intval(date('i', $ts)) . '-';
+
+        return (strpos($this->min, $currentmin) !== false);
+    }
+
+    function secMatches($ts)
+    {
+        if ($this->sec == '*') {
+            return true;
+        }
+
+        $currentsec = '-' . intval(date('s', $ts)) . '-';
+
+        return (strpos($this->sec, $currentsec) !== false);
+    }
+
+    function parse($str)
+    {
+        $s = array();
+
+        list($s['sec'], $s['min'], $s['hour'], $s['day'], $s['month']) = preg_split('|[\n\t ]+|', $str);
+
+        foreach ($s as $k => $v) {
+            if (strpos($v, '*') !== false) {
+                $s[$k] = array('*');
+            } elseif (!$this->generallyDecentSyntax($v)) {
+                die("Illegal syntax in '$v'\n");
+            } else {
+                $s[$k] = explode(',', $s[$k]);
+            }
+        }
+
+        if ($s['sec'][0] == '*') {
+            $this->sec = '*';
+        } else {
+            for ($i = 0; $i < sizeof($s['sec']); $i++) {
+                if ($this->isRange($s['sec'][$i])) {
+                    $s['sec'][$i] = $this->expandRange($this->rangeVals($s['sec'][$i]));
+                }
+            }
+            $this->sec = '-' . join('-', $s['sec']) . '-';
+        }
+
+        if ($s['min'][0] == '*') {
+            $this->min = '*';
+        } else {
+            for ($i = 0; $i < sizeof($s['min']); $i++) {
+                if ($this->isRange($s['min'][$i])) {
+                    $s['min'][$i] = $this->expandRange($this->rangeVals($s['min'][$i]));
+                }
+            }
+            $this->min = '-' . join('-', $s['min']) . '-';
+        }
+
+        if ($s['hour'][0] == '*') {
+            $this->hour = '*';
+        } else {
+            for ($i = 0; $i < sizeof($s['hour']); $i++) {
+                if ($this->isRange($s['hour'][$i])) {
+                    $s['hour'][$i] = $this->expandRange($this->rangeVals($s['hour'][$i]));
+                }
+            }
+            $this->hour = '-' . join('-', $s['hour']) . '-';
+        }
+
+        if ($s['day'][0] == '*') {
+            $this->day = '*';
+        } else {
+            for ($i = 0; $i < sizeof($s['day']); $i++) {
+                $tmp = array();
+                if (($char = $this->isCond($s['day'][$i])) !== false) {
+                    if ($char == '&') {
+                        list($tmp['value'], $tmp['and']) = explode($char, $s['day'][$i]);
+                        if ($this->isRange($tmp['and'])) {
+                            $tmp['and'] = $this->expandRange($this->rangeVals($tmp['and']));
+                        }
+                    } else {
+                        list($tmp['value'], $tmp['not']) = explode($char, $s['day'][$i]);
+                        if ($this->isRange($tmp['not'])) {
+                            $tmp['not'] = $this->expandRange($this->rangeVals($tmp['not']));
+                        }
+                    }
+                } else {
+                    $tmp = array('value' => $s['day'][$i]);
+                }
+
+                $s['day'][$i] = $tmp;
+
+                if ($this->isRange($s['day'][$i]['value'])) {
+                    $s['day'][$i]['value'] = $this->expandRange($this->rangeVals($s['day'][$i]['value']));
+                }
+            }
+
+            $this->day = $s['day'];
+        }
+
+        if ($s['month'][0] == '*') {
+            $this->month = '*';
+        } else {
+            for ($i = 0; $i < sizeof($s['month']); $i++) {
+                if ($this->isRange($s['month'][$i])) {
+                    $s['month'][$i] = $this->expandRange($this->rangeVals($s['month'][$i]));
+                }
+            }
+            $this->month = '-' . join('-', $s['month']) . '-';
+        }
+    }
+
+    function isCond($s)
+    {
+        if (strpos($s, '&') !== false) {
+            return '&';
+        } elseif (strpos($s, '!') !== false) {
+            return '!';
+        } else {
+            return false;
+        }
+    }
+
+    function isRange($s)
+    {
+        return preg_match('/^\w+\-\w+/', $s);
+    }
+
+    function isCondRange($s)
+    {
+        return (isCond($s) && isRange($s));
+    }
+
+    function isCondVal($s)
+    {
+        return (isCond($s) && !isRange($s));
+    }
+
+    function rangeVals($s)
+    {
+        return explode('-', $s);
+    }
+
+    function expandRange($l, $h = '')
+    {
+        // Expand range from 1-5 -> '-1-2-3-4-5-'.
+        if (is_array($l)) {
+            $h = $l[1];
+            $l = $l[0];
+        }
+
+        if ($this->isDigit($l)) {
+            if (!$this->isDigit($h)) {
+                die("Invalid value '$h' in range '$l-$h'");
+            }
+
+            // Currently there is no possible reason to need to do a
+            // range beyond 0-59 for anything.
+            if ($l < 0) {
+                $l = 0;
+            } elseif ($l > 59) {
+                $l = 59;
+            }
+
+            if ($h < 0) {
+                $h = 0;
+            } elseif ($h > 59) {
+                $h = 59;
+            }
+
+            if ($l > $h) {
+                $tmp = $l;
+                $l = $h;
+                $h = $tmp;
+                unset($tmp);
+            }
+
+            // For some reason range() doesn't work w/o the explicit
+            // intval() calls.
+            return '-' . join('-', range(intval($l), intval($h))) . '-';
+        } else {
+            // Invalid.
+            die("Invalid value '$l' in range '$l-$h'");
+        }
+    }
+
+    function dayValue($s)
+    {
+        for ($i = 0; $i < count($this->legalDays); $i++) {
+            if ($this->legalDays[$i] == $s) {
+                return $i;
+            }
+        }
+
+        return -1;
+    }
+
+    function isDigit($s)
+    {
+        return preg_match('/^\d+$/', $s);
+    }
+
+    function isAlpha($s)
+    {
+        return $this->isLegalDay($s);
+    }
+
+    function isLegalDay($s)
+    {
+        return in_array($s, $this->legalDays);
+    }
+
+    function generallyDecentSyntax($s)
+    {
+        return ($s == '*' ||
+                preg_match('/^\d+(-\d+)?([!&][A-Z\*]+(-[A-Z\*]+)?)?(,\d+(-\d+)?([!&][A-Z\*]+(-[A-Z\*]+)?)?)*$/', $s));
+    }
+
+}
diff --git a/framework/Scheduler/package.xml b/framework/Scheduler/package.xml
new file mode 100644 (file)
index 0000000..c73fe7c
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Scheduler</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Scheduler System</summary>
+ <description>TODO
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>23:07:47</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="scripts">
+    <dir name="Horde">
+     <dir name="Scheduler">
+      <file name="horde-crond.php" role="script">
+       <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+      </file>
+     </dir> <!-- /scripts/Horde/Scheduler -->
+    </dir> <!-- /scripts/Horde -->
+   </dir> <!-- /scripts -->
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Scheduler">
+      <file name="cron.php" role="php" />
+     </dir> <!-- /lib/Horde/Scheduler -->
+     <file name="Scheduler.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>VFS</name>
+    <channel>pear.php.net</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="scripts/Horde/Scheduler/horde-crond.php" as="horde-crond" />
+   <install name="lib/Horde/Scheduler/cron.php" as="Horde/Scheduler/cron.php" />
+   <install name="lib/Horde/Scheduler.php" as="Horde/Scheduler.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-05</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Scheduler/scripts/Horde/Scheduler/horde-crond.php b/framework/Scheduler/scripts/Horde/Scheduler/horde-crond.php
new file mode 100755 (executable)
index 0000000..d7785a2
--- /dev/null
@@ -0,0 +1,33 @@
+#!@php_bin@
+<?php
+/**
+ * $Horde: framework/Scheduler/scripts/Horde/Scheduler/horde-crond.php,v 1.4 2009/06/10 19:57:55 slusarz Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Horde_Scheduler
+ */
+
+require_once 'Horde/Cli.php';
+require_once 'Horde/Scheduler.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+Horde_Cli::init();
+
+// Get an instance of the cron scheduler.
+$daemon = Horde_Scheduler::factory('cron');
+
+// Now add some cron jobs to do, or add parsing to read a config file.
+// $daemon->addTask('ls', '0,5,10,15,20,30,40 * * * *');
+
+// Start the daemon going.
+$daemon->run();
diff --git a/framework/Share/Share.php b/framework/Share/Share.php
new file mode 100644 (file)
index 0000000..3f8d020
--- /dev/null
@@ -0,0 +1,926 @@
+<?php
+/**
+ * Horde_Share:: provides an interface to all shares a user might have.  Its
+ * methods take care of any site-specific restrictions configured in in the
+ * application's prefs.php and conf.php files.
+ *
+ * $Horde: framework/Share/Share.php,v 1.186 2009/12/18 13:49:01 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ * Copyright 2002-2007 Infoteck Internet <webmaster@infoteck.qc.ca>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @author  Mike Cochrame <mike@graftonhall.co.nz>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.0
+ * @package Horde_Share
+ */
+class Horde_Share {
+
+    /**
+     * The application we're managing shares for.
+     *
+     * @var string
+     */
+    var $_app;
+
+    /**
+     * The root of the Share tree.
+     *
+     * @var mixed
+     */
+    var $_root = null;
+
+    /**
+     * A cache of all shares that have been retrieved, so we don't hit the
+     * backend again and again for them.
+     *
+     * @var array
+     */
+    var $_cache = array();
+
+    /**
+     * Id-name-map of already cached share objects.
+     *
+     * @var array
+     */
+    var $_shareMap = array();
+
+    /**
+     * Cache used for listShares().
+     *
+     * @var array
+     */
+    var $_listcache = array();
+
+    /**
+     * A list of objects that we're currently sorting, for reference during the
+     * sorting algorithm.
+     *
+     * @var array
+     */
+    var $_sortList;
+
+    /**
+     * Attempts to return a reference to a concrete Horde_Share instance.
+     *
+     * It will only create a new instance if no Horde_Share instance currently
+     * exists.
+     *
+     * @param string $app     The application that the shares relates to.
+     * @param string $driver  Type of concrete Share subclass to return,
+     *                        based on storage driver ($driver). The code is
+     *                        dynamically included.
+     *
+     * @return Horde_Share  The concrete Share reference, or false on an error.
+     */
+    public static function singleton($app, $driver = null)
+    {
+        static $shares = array();
+
+        // FIXME: This is a temporary solution until the configuration value
+        // actually exists and all apps call this code in the correct fashion.
+        $driver = basename($driver);
+        if (empty($driver)) {
+            if (!empty($GLOBALS['conf']['share']['driver'])) {
+                $driver = $GLOBALS['conf']['share']['driver'];
+            } else {
+                $driver = 'datatree';
+            }
+        }
+
+        $class = 'Horde_Share_' . $driver;
+        if (!class_exists($class)) {
+            include dirname(__FILE__) . '/Share/' . $driver . '.php';
+        }
+
+        $signature = $app . '_' . $driver;
+        if (!isset($shares[$signature]) &&
+            !empty($GLOBALS['conf']['share']['cache'])) {
+            require_once 'Horde/SessionObjects.php';
+            $session = Horde_SessionObjects::singleton();
+            $shares[$signature] = $session->query('horde_share_' . $app . '_' . $driver . '1');
+        }
+
+        if (empty($shares[$signature])) {
+            if (class_exists($class)) {
+                $shares[$signature] = new $class($app);
+            } else {
+                $result = PEAR::raiseError(sprintf(_("\"%s\" share driver not found."), $driver));
+                return $result;
+            }
+        }
+
+        if (!empty($GLOBALS['conf']['share']['cache'])) {
+            register_shutdown_function(array($shares[$signature], 'shutdown'));
+        }
+
+        return $shares[$signature];
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param string $app  The application that the shares belong to.
+     */
+    function Horde_Share($app)
+    {
+        $this->_app = $app;
+        $this->__wakeup();
+    }
+
+    /**
+     * Initializes the object.
+     *
+     * @throws Horde_Exception
+     */
+    function __wakeup()
+    {
+        try {
+            Horde::callHook('share_init', array($this, $this->_app));
+        } catch (Horde_Exception_HookNotSet $e) {}
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_sortList']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Stores the object in the session cache.
+     */
+    function shutdown()
+    {
+        $driver = str_replace('horde_share_', '', Horde_String::lower(get_class($this)));
+        require_once 'Horde/SessionObjects.php';
+        $session = Horde_SessionObjects::singleton();
+        $session->overwrite('horde_share_' . $this->_app . '_' . $driver, $this, false);
+    }
+
+    /**
+     * Returns the application we're managing shares for.
+     *
+     * @return string  The application this share belongs to.
+     */
+    function getApp()
+    {
+        return $this->_app;
+    }
+
+    /**
+     * Returns a Horde_Share_Object object corresponding to the given share
+     * name, with the details retrieved appropriately.
+     *
+     * @param string $name  The name of the share to retrieve.
+     *
+     * @return Horde_Share_Object  The requested share.
+     */
+    function getShare($name)
+    {
+        if (isset($this->_cache[$name])) {
+            return $this->_cache[$name];
+        }
+
+        $share = $this->_getShare($name);
+        if (is_a($share, 'PEAR_Error')) {
+            return $share;
+        }
+        $share->setShareOb($this);
+        $this->_shareMap[$share->getId()] = $name;
+        $this->_cache[$name] = $share;
+
+        return $share;
+    }
+
+    /**
+     * Returns a Horde_Share_Object object corresponding to the given unique
+     * ID, with the details retrieved appropriately.
+     *
+     * @param string $cid  The id of the share to retrieve.
+     *
+     * @return Horde_Share_Object  The requested share.
+     */
+    function getShareById($cid)
+    {
+        if (!isset($this->_shareMap[$cid])) {
+            $share = $this->_getShareById($cid);
+            if (is_a($share, 'PEAR_Error')) {
+                return $share;
+            }
+            $share->setShareOb($this);
+            $name = $share->getName();
+            $this->_cache[$name] = $share;
+            $this->_shareMap[$cid] = $name;
+        }
+
+        return $this->_cache[$this->_shareMap[$cid]];
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object objects corresponding to the
+     * given set of unique IDs, with the details retrieved appropriately.
+     *
+     * @param array $cids  The array of ids to retrieve.
+     *
+     * @return array  The requested shares.
+     */
+    function getShares($cids)
+    {
+        $all_shares = array();
+        $missing_ids = array();
+        foreach ($cids as $cid) {
+            if (isset($this->_shareMap[$cid])) {
+                $all_shares[$this->_shareMap[$cid]] = $this->_cache[$this->_shareMap[$cid]];
+            } else {
+                $missing_ids[] = $cid;
+            }
+        }
+
+        if (count($missing_ids)) {
+            $shares = $this->_getShares($missing_ids);
+            if (is_a($shares, 'PEAR_Error')) {
+                return $shares;
+            }
+
+            foreach (array_keys($shares) as $key) {
+                $this->_cache[$key] = $shares[$key];
+                $this->_cache[$key]->setShareOb($this);
+                $this->_shareMap[$shares[$key]->getId()] = $key;
+                $all_shares[$key] = $this->_cache[$key];
+            }
+        }
+
+        return $all_shares;
+    }
+
+    /**
+     * Lists *all* shares for the current app/share, regardless of
+     * permissions.
+     *
+     * This is for admin functionality and scripting tools, and shouldn't be
+     * called from user-level code!
+     *
+     * @return array  All shares for the current app/share.
+     */
+    function listAllShares()
+    {
+        $shares = $this->_listAllShares();
+        if (is_a($shares, 'PEAR_Error') || !count($shares)) {
+            return $shares;
+        }
+
+        $this->_sortList = $shares;
+        uasort($shares, array($this, '_sortShares'));
+        $this->_sortList = null;
+
+        return $shares;
+    }
+
+    /**
+     * Returns an array of all shares that $userid has access to.
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return array  The shares the user has access to.
+     */
+    function listShares($userid, $perm = Horde_Perms::SHOW, $attributes = null,
+                        $from = 0, $count = 0, $sort_by = null, $direction = 0)
+    {
+        $shares = $this->_listShares($userid, $perm, $attributes, $from,
+                                     $count, $sort_by, $direction);
+        if (!count($shares)) {
+            return $shares;
+        }
+        if (is_a($shares, 'PEAR_Error')) {
+            return $shares;
+        }
+
+        /* Make sure getShares() didn't return an error. */
+        $shares = $this->getShares($shares);
+        if (is_a($shares, 'PEAR_Error')) {
+            return $shares;
+        }
+
+        if (is_null($sort_by)) {
+            $this->_sortList = $shares;
+            uasort($shares, array($this, '_sortShares'));
+            $this->_sortList = null;
+        }
+
+        try {
+            return Horde::callHook('share_list', array($userid, $perm, $attributes, $shares));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        return $shares;
+    }
+
+    /**
+     * Returns an array of all system shares.
+     *
+     * @return array  All system shares.
+     */
+    function listSystemShares()
+    {
+        return array();
+    }
+
+    /**
+     * Returns the number of shares that $userid has access to.
+     *
+     * @since Horde 3.2
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return integer  The number of shares
+     */
+    function countShares($userid, $perm = Horde_Perms::SHOW, $attributes = null)
+    {
+        return $this->_countShares($userid, $perm, $attributes);
+    }
+
+    /**
+     * Returns a new share object.
+     *
+     * @param string $name  The share's name.
+     *
+     * @return Horde_Share_Object  A new share object.
+     */
+    function newShare($name)
+    {
+        if (empty($name)) {
+            return PEAR::raiseError('Share names must be non-empty');
+        }
+        $share = $this->_newShare($name);
+        $share->setShareOb($this);
+        $share->set('owner', Horde_Auth::getAuth());
+
+        return $share;
+    }
+
+    /**
+     * Adds a share to the shares system.
+     *
+     * The share must first be created with Horde_Share::newShare(), and have
+     * any initial details added to it, before this function is called.
+     *
+     * @param Horde_Share_Object $share  The new share object.
+     * @throws Horde_Exception
+     */
+    function addShare($share)
+    {
+        if (!is_a($share, 'Horde_Share_Object')) {
+            return PEAR::raiseError('Shares must be Horde_Share_Object objects or extend that class.');
+        }
+
+        try {
+            Horde::callHook('share_add', array($share));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        $result = $this->_addShare($share);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Store new share in the caches. */
+        $id = $share->getId();
+        $name = $share->getName();
+        $this->_cache[$name] = $share;
+        $this->_shareMap[$id] = $name;
+
+        /* Reset caches that depend on unknown criteria. */
+        $this->_listCache = array();
+
+        return $result;
+    }
+
+    /**
+     * Removes a share from the shares system permanently.
+     *
+     * @param Horde_Share_Object $share  The share to remove.
+     *
+     * @throws Horde_Exception
+     */
+    function removeShare($share)
+    {
+        if (!is_a($share, 'Horde_Share_Object')) {
+            return PEAR::raiseError('Shares must be Horde_Share_Object objects or extend that class.');
+        }
+
+        try {
+            Horde::callHook('share_remove', array($share));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        /* Remove share from the caches. */
+        $id = $share->getId();
+        unset($this->_shareMap[$id]);
+        unset($this->_cache[$share->getName()]);
+
+        /* Reset caches that depend on unknown criteria. */
+        $this->_listCache = array();
+
+        return $this->_removeShare($share);
+    }
+
+    /**
+     * Checks if a share exists in the system.
+     *
+     * @param string $share  The share to check.
+     *
+     * @return boolean  True if the share exists.
+     */
+    function exists($share)
+    {
+        if (isset($this->_cache[$share])) {
+            return true;
+        }
+
+        return $this->_exists($share);
+    }
+
+    /**
+     * Finds out what rights the given user has to this object.
+     *
+     * @see Perms::getPermissions
+     *
+     * @param mixed $share  The share that should be checked for the users
+     *                      permissions.
+     * @param string $user  The user to check for.
+     *
+     * @return mixed  A bitmask of permissions, a permission value, or an array
+     *                of permission values the user has, depending on the
+     *                permission type and whether the permission value is
+     *                ambiguous. False if there is no such permsission.
+     */
+    function getPermissions($share, $user = null)
+    {
+        if (is_a($share, 'PEAR_Error')) {
+            Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+
+        if (!is_a($share, 'Horde_Share_Object')) {
+            $share = $this->getShare($share);
+            if (is_a($share, 'PEAR_Error')) {
+                Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return false;
+            }
+        }
+
+        $perm = $share->getPermission();
+        return $GLOBALS['perms']->getPermissions($perm, $user);
+    }
+
+    /**
+     * Returns the Identity for a particular share owner.
+     *
+     * @deprecated
+     *
+     * @param mixed $share  The share to fetch the Identity for - either the
+     *                      string name, or the Horde_Share_Object object.
+     *
+     * @return Identity  An Identity instance.
+     */
+    function getIdentityByShare($share)
+    {
+        if (!is_a($share, 'Horde_Share_Object')) {
+            $share = $this->getShare($share);
+            if (is_a($share, 'PEAR_Error')) {
+                return null;
+            }
+        }
+
+        return Horde_Prefs_Identity::singleton('none', $share->get('owner'));
+    }
+
+    /**
+     * Utility function to be used with uasort() for sorting arrays of
+     * Horde_Share objects.
+     *
+     * Example:
+     * <code>
+     * uasort($list, array('Horde_Share', '_sortShares'));
+     * </code>
+     *
+     * @access protected
+     */
+    function _sortShares($a, $b)
+    {
+        $aParts = explode(':', $a->getName());
+        $bParts = explode(':', $b->getName());
+
+        $min = min(count($aParts), count($bParts));
+        $idA = '';
+        $idB = '';
+        for ($i = 0; $i < $min; $i++) {
+            if ($idA) {
+                $idA .= ':';
+                $idB .= ':';
+            }
+            $idA .= $aParts[$i];
+            $idB .= $bParts[$i];
+
+            if ($idA != $idB) {
+                $curA = isset($this->_sortList[$idA]) ? $this->_sortList[$idA]->get('name') : '';
+                $curB = isset($this->_sortList[$idB]) ? $this->_sortList[$idB]->get('name') : '';
+                return strnatcasecmp($curA, $curB);
+            }
+        }
+
+        return count($aParts) > count($bParts);
+    }
+
+}
+
+/**
+ * Abstract class for storing Share information.
+ *
+ * This class should be extended for the more specific drivers.
+ *
+ * @author  Mike Cochrane <mike@graftonhall.co.nz>
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_Object {
+
+    /**
+     * The Horde_Share object which this share came from - needed for updating
+     * data in the backend to make changes stick, etc.
+     *
+     * @var Horde_Share
+     */
+    var $_shareOb;
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_shareOb']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Associates a Share object with this share.
+     *
+     * @param Horde_Share $shareOb  The Share object.
+     */
+    function setShareOb($shareOb)
+    {
+        if (!is_a($shareOb, 'Horde_Share')) {
+            return PEAR::raiseError('This object needs a Horde_Share instance as storage handler!');
+        }
+        $this->_shareOb = $shareOb;
+    }
+
+    /**
+     * Sets an attribute value in this object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     *
+     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
+     *                otherwise.
+     */
+    function set($attribute, $value)
+    {
+        return $this->_set($attribute, $value);
+    }
+
+    /**
+     * Returns an attribute value from this object.
+     *
+     * @param string $attribute  The attribute to return.
+     *
+     * @return mixed  The value for $attribute.
+     */
+    function get($attribute)
+    {
+        return $this->_get($attribute);
+    }
+
+    /**
+     * Returns the ID of this share.
+     *
+     * @return string  The share's ID.
+     */
+    function getId()
+    {
+        return $this->_getId();
+    }
+
+    /**
+     * Returns the name of this share.
+     *
+     * @return string  The share's name.
+     */
+    function getName()
+    {
+        return $this->_getName();
+    }
+
+    /**
+     * Saves the current attribute values.
+     *
+     * @throws Horde_Exception
+     */
+    function save()
+    {
+        try {
+            Horde::callHook('share_modify', array($this));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        return $this->_save();
+    }
+
+    /**
+     * Gives a user a certain privilege for this share.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A Horde_Perms::* constant.
+     */
+    function addUserPermission($userid, $permission)
+    {
+        $perm = $this->getPermission();
+        $perm->addUserPermission($userid, $permission, false);
+        $this->setPermission($perm);
+    }
+
+    /**
+     * Removes a certain privilege for a user from this share.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A Horde_Perms::* constant.
+     */
+    function removeUserPermission($userid, $permission)
+    {
+        $perm = $this->getPermission();
+        $perm->removeUserPermission($userid, $permission, false);
+        $this->setPermission($perm);
+    }
+
+    /**
+     * Gives a group certain privileges for this share.
+     *
+     * @param string $group        The group to add permissions for.
+     * @param integer $permission  A Horde_Perms::* constant.
+     */
+    function addGroupPermission($group, $permission)
+    {
+        $perm = $this->getPermission();
+        $perm->addGroupPermission($group, $permission, false);
+        $this->setPermission($perm);
+    }
+
+    /**
+     * Removes a certain privilege from a group.
+     *
+     * @param string $group         The group to remove permissions from.
+     * @param constant $permission  A Horde_Perms::* constant.
+     */
+    function removeGroupPermission($group, $permission)
+    {
+        $perm = $this->getPermission();
+        $perm->removeGroupPermission($group, $permission, false);
+        $this->setPermission($perm);
+    }
+
+    /**
+     * Removes a user from this share.
+     *
+     * @param string $userid  The userid of the user to remove.
+     */
+    function removeUser($userid)
+    {
+        /* Remove all $userid's permissions. */
+        $perm = $this->getPermission();
+        $perm->removeUserPermission($userid, Horde_Perms::SHOW, false);
+        $perm->removeUserPermission($userid, Horde_Perms::READ, false);
+        $perm->removeUserPermission($userid, Horde_Perms::EDIT, false);
+        $perm->removeUserPermission($userid, Horde_Perms::DELETE, false);
+        return $this->setPermission($perm);
+    }
+
+    /**
+     * Removes a group from this share.
+     *
+     * @param integer $groupId  The group to remove.
+     */
+    function removeGroup($groupId)
+    {
+        /* Remove all $groupId's permissions. */
+        $perm = $this->getPermission();
+        $perm->removeGroupPermission($groupId, Horde_Perms::SHOW, false);
+        $perm->removeGroupPermission($groupId, Horde_Perms::READ, false);
+        $perm->removeGroupPermission($groupId, Horde_Perms::EDIT, false);
+        $perm->removeGroupPermission($groupId, Horde_Perms::DELETE, false);
+        return $this->setPermission($perm);
+    }
+
+    /**
+     * Returns an array containing all the userids of the users with access to
+     * this share.
+     *
+     * @param integer $perm_level  List only users with this permission level.
+     *                             Defaults to all users.
+     *
+     * @return array  The users with access to this share.
+     */
+    function listUsers($perm_level = null)
+    {
+        $perm = $this->getPermission();
+        $results = array_keys($perm->getUserPermissions($perm_level));
+        // Always return the share's owner.
+        if ($this->get('owner')) {
+            array_push($results, $this->get('owner'));
+        }
+        return $results;
+    }
+
+    /**
+     * Returns an array containing all the groupids of the groups with access
+     * to this share.
+     *
+     * @param integer $perm_level  List only users with this permission level.
+     *                             Defaults to all users.
+     *
+     * @return array  The IDs of the groups with access to this share.
+     */
+    function listGroups($perm_level = null)
+    {
+        $perm = $this->getPermission();
+        return array_keys($perm->getGroupPermissions($perm_level));
+    }
+
+    /**
+     * Locks an item from this share, or the entire share if no item defined.
+     *
+     * @param string $item_uid  A uid of an item from this share.
+     *
+     * @return mixed   A lock ID on success, PEAR_Error on failure, false if:
+     *                  - The share is already locked
+     *                  - The item is already locked
+     *                  - A share lock was requested and an item is already
+     *                    locked in the share
+     */
+    function lock($item_uid = null)
+    {
+        require_once 'Horde/Lock.php';
+
+        try {
+            $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        } catch (Horde_Lock_Exception $e) {
+            $locks = PEAR::raiseError($e->getMessage());
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $locks;
+        }
+
+        $shareid = $this->getId();
+
+        // Default parameters.
+        $locktype = Horde_Lock::TYPE_EXCLUSIVE;
+        $timeout = 600;
+        $itemscope = $this->_shareOb->getApp() . ':' . $shareid;
+
+        if (!empty($item_uid)) {
+            // Check if the share is locked. Share locks are placed at app
+            // scope.
+            try {
+
+                $result = $locks->getLocks($this->_shareOb->getApp(), $shareid, $locktype);
+            } catch (Horde_Lock_Exception $e) {
+                return PEAR::raiseError($e->getMessage);
+            }
+            if (!empty($result)) {
+                // Lock found.
+                return false;
+            }
+            // Try to place the item lock at app:shareid scope.
+            return $locks->setLock(Horde_Auth::getAuth(), $itemscope, $item_uid,
+                                   $timeout, $locktype);
+        } else {
+            // Share lock requested. Check for locked items.
+            try {
+                $result = $locks->getLocks($itemscope, null, $locktype);
+            } catch (Horde_Lock_Exception $e) {
+                return PEAR::raiseError($e->getException);
+            }
+            if (!empty($result)) {
+                // Lock found.
+                return false;
+            }
+            // Try to place the share lock
+            return $locks->setLock(Horde_Auth::getAuth(), $this->_shareOb->getApp(),
+                                   $shareid, $timeout, $locktype);
+        }
+    }
+
+    /**
+     * Removes the lock for a lock ID.
+     *
+     * @param string $lockid  The lock ID as generated by a previous call
+     *                        to lock().
+     *
+     * @return mixed  True on success, PEAR_Error on failure.
+     */
+    function unlock($lockid)
+    {
+        require_once 'Horde/Lock.php';
+
+        try {
+            $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        } catch (Horde_Lock_Exception $e) {
+            $locks = PEAR::raiseError($e->getMessage());
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $locks;
+        }
+
+        return $locks->clearLock($lockid);
+    }
+
+    /**
+     * Checks for existing locks.
+     *
+     * First this checks for share locks and if none exists, checks for item
+     * locks (if item_uid defined).  It will return the first lock found.
+     *
+     * @param string $item_uid  A uid of an item from this share.
+     *
+     * @return mixed   Hash with the found lock information in 'lock' and the
+     *                 lock type ('share' or 'item') in 'type', or an empty
+     *                 array if there are no locks, or a PEAR_Error on failure.
+     */
+    function checkLocks($item_uid = null)
+    {
+        require_once 'Horde/Lock.php';
+        try {
+            $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        } catch (Horde_Lock_Exception $e) {
+            $locks = PEAR::raiseError($e->getMessage());
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $locks;
+        }
+
+        $shareid = $this->getId();
+        $locktype = Horde_Lock::TYPE_EXCLUSIVE;
+
+        // Check for share locks
+        try {
+            $result = $locks->getLocks($this->_shareOb->getApp(), $shareid, $locktype);
+        } catch (Horde_Lock_Exception $e) {
+            $result = PEAR::raiseError($e->getMessage());
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        if (empty($result) && !empty($item_uid)) {
+            // Check for item locks
+            $locktargettype = 'item';
+            try {
+                $result = $locks->getLocks($this->_shareOb->getApp() . ':' . $shareid, $item_uid, $locktype);
+            } catch (Horde_Lock_Exception $e) {
+                $result = PEAR::raiseError($e->getMessage());
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            }
+        } else {
+            $locktargettype = 'share';
+        }
+
+        if (empty($result)) {
+            return array();
+        }
+
+        return array('type' => $locktargettype,
+                     'lock' => reset($result));
+    }
+
+}
diff --git a/framework/Share/Share/datatree.php b/framework/Share/Share/datatree.php
new file mode 100644 (file)
index 0000000..97ecf91
--- /dev/null
@@ -0,0 +1,584 @@
+<?php
+
+require_once 'Horde/DataTree.php';
+
+/**
+ * Horde_Share_datatree:: provides the datatree backend for the horde share
+ * driver.
+ *
+ * $Horde: framework/Share/Share/datatree.php,v 1.29 2009-12-10 19:24:08 mrubinsk Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ * Copyright 2002-2007 Infoteck Internet <webmaster@infoteck.qc.ca>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @author  Mike Cochrame <mike@graftonhall.co.nz>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_datatree extends Horde_Share {
+
+    /**
+     * The Horde_Share_Object subclass to instantiate objects as
+     *
+     * @var string
+     */
+    var $_shareObject = 'Horde_Share_Object_datatree';
+
+    /**
+     * Pointer to a DataTree instance to manage/store shares
+     *
+     * @var DataTree
+     */
+    var $_datatree;
+
+    /**
+     * Initializes the object.
+     *
+     * @throws Horde_Exception
+     */
+    function __wakeup()
+    {
+        if (empty($GLOBALS['conf']['datatree']['driver'])) {
+            throw new Horde_Exception('You must configure a DataTree backend to use Shares.');
+        }
+
+        $driver = $GLOBALS['conf']['datatree']['driver'];
+        $this->_datatree = &DataTree::singleton(
+            $driver,
+            array_merge(Horde::getDriverConfig('datatree', $driver),
+                        array('group' => 'horde.shares.' . $this->_app))
+        );
+
+        foreach (array_keys($this->_cache) as $name) {
+            if (!is_a($this->_datatree, 'PEAR_Error')) {
+                $this->_cache[$name]->setShareOb($this);
+                $this->_cache[$name]->datatreeObject->setDataTree($this->_datatree);
+            }
+        }
+
+        parent::__wakeup();
+    }
+
+    /**
+     * Returns a Horde_Share_Object_datatree object corresponding to the given
+     * share name, with the details retrieved appropriately.
+     *
+     * @param string $name  The name of the share to retrieve.
+     *
+     * @return Horde_Share_Object_datatree  The requested share.
+     */
+    function &_getShare($name)
+    {
+        $datatreeObject = $this->_datatree->getObject($name, 'DataTreeObject_Share');
+        if (is_a($datatreeObject, 'PEAR_Error')) {
+            return $datatreeObject;
+        }
+        $share = new $this->_shareObject($datatreeObject);
+        return $share;
+    }
+
+    /**
+     * Returns a Horde_Share_Object_datatree object corresponding to the given
+     * unique ID, with the details retrieved appropriately.
+     *
+     * @param string $cid  The id of the share to retrieve.
+     *
+     * @return Horde_Share_Object_datatree  The requested share.
+     */
+    function &_getShareById($id)
+    {
+        $datatreeObject = $this->_datatree->getObjectById($id, 'DataTreeObject_Share');
+        if (is_a($datatreeObject, 'PEAR_Error')) {
+            return $datatreeObject;
+        }
+        $share = new $this->_shareObject($datatreeObject);
+        return $share;
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object_datatree objects corresponding
+     * to the given set of unique IDs, with the details retrieved
+     * appropriately.
+     *
+     * @param array $cids  The array of ids to retrieve.
+     *
+     * @return array  The requested shares.
+     */
+    function &_getShares($ids)
+    {
+        $shares = array();
+        $objects = &$this->_datatree->getObjects($ids, 'DataTreeObject_Share');
+        if (is_a($objects, 'PEAR_Error')) {
+            return $objects;
+        }
+        foreach (array_keys($objects) as $key) {
+            if (is_a($objects[$key], 'PEAR_Error')) {
+                return $objects[$key];
+            }
+            $shares[$key] = new $this->_shareObject($objects[$key]);
+        }
+        return $shares;
+    }
+
+    /**
+     * Lists *all* shares for the current app/share, regardless of
+     * permissions.
+     *
+     * @return array  All shares for the current app/share.
+     */
+    function &_listAllShares()
+    {
+        $sharelist = $this->_datatree->get(DATATREE_FORMAT_FLAT, DATATREE_ROOT,
+                                           true);
+        if (is_a($sharelist, 'PEAR_Error') || !count($sharelist)) {
+            // If we got back an error or an empty array, just return it.
+            return $sharelist;
+        }
+        unset($sharelist[DATATREE_ROOT]);
+
+        return $this->getShares(array_keys($sharelist));
+    }
+
+    /**
+     * Returns an array of all shares that $userid has access to.
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return array  The shares the user has access to.
+     */
+    function &_listShares($userid, $perm = Horde_Perms::SHOW,
+                          $attributes = null, $from = 0, $count = 0,
+                          $sort_by = null, $direction = 0)
+    {
+        $key = serialize(array($userid, $perm, $attributes));
+        if (empty($this->_listCache[$key])) {
+            $criteria = $this->_getShareCriteria($userid, $perm, $attributes);
+            $sharelist = $this->_datatree->getByAttributes($criteria,
+                                                           DATATREE_ROOT,
+                                                           true, 'id', $from,
+                                                           $count, $sort_by,
+                                                           null, $direction);
+            if (is_a($sharelist, 'PEAR_Error')) {
+                return $sharelist;
+            }
+            $this->_listCache[$key] = array_keys($sharelist);
+        }
+
+        return $this->_listCache[$key];
+    }
+
+    /**
+     * Returns the number of shares that $userid has access to.
+     *
+     * @since Horde 3.2
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return integer  The number of shares
+     */
+    function _countShares($userid, $perm = Horde_Perms::SHOW,
+                          $attributes = null)
+    {
+        $criteria = $this->_getShareCriteria($userid, $perm, $attributes);
+        return $this->_datatree->countByAttributes($criteria, DATATREE_ROOT, true, 'id');
+    }
+
+    /**
+     * Returns a new share object.
+     *
+     * @param string $name  The share's name.
+     *
+     * @return Horde_Share_Object_datatree  A new share object.
+     */
+    function &_newShare($name)
+    {
+        $datatreeObject = new DataTreeObject_Share($name);
+        $datatreeObject->setDataTree($this->_datatree);
+        $share = new $this->_shareObject($datatreeObject);
+        return $share;
+    }
+
+    /**
+     * Adds a share to the shares system.
+     *
+     * The share must first be created with
+     * Horde_Share_datatreee::_newShare(), and have any initial details added
+     * to it, before this function is called.
+     *
+     * @param Horde_Share_Object_datatree $share  The new share object.
+     */
+    function _addShare(&$share)
+    {
+        return $this->_datatree->add($share->datatreeObject);
+    }
+
+    /**
+     * Removes a share from the shares system permanently.
+     *
+     * @param Horde_Share_Object_datatree $share  The share to remove.
+     */
+    function _removeShare(&$share)
+    {
+        return $this->_datatree->remove($share->datatreeObject);
+    }
+
+    /**
+     * Checks if a share exists in the system.
+     *
+     * @param string $share  The share to check.
+     *
+     * @return boolean  True if the share exists.
+     */
+    function _exists($share)
+    {
+        return $this->_datatree->exists($share);
+    }
+
+    /**
+     * Returns an array of criteria for querying shares.
+     * @access protected
+     *
+     * @param string  $userid      The userid of the user to check access for.
+     * @param integer $perm        The level of permissions required.
+     * @param mixed   $attributes  Restrict the shares returned to those who
+     *                             have these attribute values.
+     *
+     * @return array  The criteria tree for fetching this user's shares.
+     */
+    function _getShareCriteria($userid, $perm = Horde_Perms::SHOW,
+                               $attributes = null)
+    {
+        if (!empty($userid)) {
+            $criteria = array(
+                'OR' => array(
+                    // (owner == $userid)
+                    array(
+                        'AND' => array(
+                            array('field' => 'name', 'op' => '=', 'test' => 'owner'),
+                            array('field' => 'value', 'op' => '=', 'test' => $userid))),
+
+                    // (name == perm_users and key == $userid and val & $perm)
+                    array(
+                        'AND' => array(
+                            array('field' => 'name', 'op' => '=', 'test' => 'perm_users'),
+                            array('field' => 'key', 'op' => '=', 'test' => $userid),
+                            array('field' => 'value', 'op' => '&', 'test' => $perm))),
+
+                    // (name == perm_creator and val & $perm)
+                    array(
+                        'AND' => array(
+                            array('field' => 'name', 'op' => '=', 'test' => 'perm_creator'),
+                            array('field' => 'value', 'op' => '&', 'test' => $perm))),
+
+                    // (name == perm_default and val & $perm)
+                    array(
+                        'AND' => array(
+                            array('field' => 'name', 'op' => '=', 'test' => 'perm_default'),
+                            array('field' => 'value', 'op' => '&', 'test' => $perm)))));
+
+            // If the user has any group memberships, check for those also.
+            require_once 'Horde/Group.php';
+            $group = &Group::singleton();
+            $groups = $group->getGroupMemberships($userid, true);
+            if (!is_a($groups, 'PEAR_Error') && $groups) {
+                // (name == perm_groups and key in ($groups) and val & $perm)
+                $criteria['OR'][] = array(
+                    'AND' => array(
+                        array('field' => 'name', 'op' => '=', 'test' => 'perm_groups'),
+                        array('field' => 'key', 'op' => 'IN', 'test' => array_keys($groups)),
+                        array('field' => 'value', 'op' => '&', 'test' => $perm)));
+            }
+        } else {
+            $criteria = array(
+                'AND' => array(
+                     array('field' => 'name', 'op' => '=', 'test' => 'perm_guest'),
+                     array('field' => 'value', 'op' => '&', 'test' => $perm)));
+        }
+
+        if (is_array($attributes)) {
+            // Build attribute/key filter.
+            foreach ($attributes as $key => $value) {
+                $criteria = array(
+                    'AND' => array(
+                        $criteria,
+                        array(
+                            'JOIN' => array(
+                                'AND' => array(
+                                    array('field' => 'name', 'op' => '=', 'test' => $key),
+                                    array('field' => 'value', 'op' => '=', 'test' => $value))))));
+            }
+        } elseif (!is_null($attributes)) {
+            // Restrict to shares owned by the user specified in the
+            // $attributes string.
+            $criteria = array(
+                'AND' => array(
+                    $criteria,
+                    array(
+                        'JOIN' => array(
+                            array('field' => 'name', 'op' => '=', 'test' => 'owner'),
+                            array('field' => 'value', 'op' => '=', 'test' => $attributes)))));
+        }
+
+        return $criteria;
+    }
+
+}
+
+/**
+ * Extension of the Horde_Share_Object class for storing share information in
+ * the DataTree driver.
+ *
+ * @author  Mike Cochrane <mike@graftonhall.co.nz>
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_Object_datatree extends Horde_Share_Object {
+
+    /**
+     * The actual storage object that holds the data.
+     *
+     * @var mixed
+     */
+    var $datatreeObject;
+
+    /**
+     * Constructor.
+     *
+     * @param DataTreeObject_Share $datatreeObject  A DataTreeObject_Share
+     *                                              instance.
+     */
+    function Horde_Share_Object_datatree($datatreeObject)
+    {
+        if (is_a($datatreeObject, 'PEAR_Error')) {
+            debug_context();
+        }
+        $this->datatreeObject = $datatreeObject;
+    }
+
+    /**
+     * Sets an attribute value in this object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     *
+     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
+     *                otherwise.
+     */
+    function _set($attribute, $value)
+    {
+        return $this->datatreeObject->set($attribute, $value);
+    }
+
+    /**
+     * Returns one of the attributes of the object, or null if it isn't
+     * defined.
+     *
+     * @param string $attribute  The attribute to retrieve.
+     *
+     * @return mixed  The value of the attribute, or an empty string.
+     */
+    function _get($attribute)
+    {
+        return $this->datatreeObject->get($attribute);
+    }
+
+    /**
+     * Returns the ID of this share.
+     *
+     * @return string  The share's ID.
+     */
+    function _getId()
+    {
+        return $this->datatreeObject->getId();
+    }
+
+    /**
+     * Returns the name of this share.
+     *
+     * @return string  The share's name.
+     */
+    function _getName()
+    {
+        return $this->datatreeObject->getName();
+    }
+
+    /**
+     * Saves the current attribute values.
+     */
+    function _save()
+    {
+        return $this->datatreeObject->save();
+    }
+
+    /**
+     * Checks to see if a user has a given permission.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A Horde_Perms::* constant to test for.
+     * @param string $creator      The creator of the event.
+     *
+     * @return boolean  Whether or not $userid has $permission.
+     */
+    function hasPermission($userid, $permission, $creator = null)
+    {
+        if ($userid && $userid == $this->datatreeObject->get('owner')) {
+            return true;
+        }
+
+        return $GLOBALS['perms']->hasPermission($this->getPermission(),
+                                                $userid, $permission, $creator);
+    }
+
+    /**
+     * Sets the permission of this share.
+     *
+     * @param Horde_Perms_Permission $perm  Permission object.
+     * @param boolean $update         Should the share be saved
+     *                                after this operation?
+     *
+     * @return boolean  True if no error occured, PEAR_Error otherwise
+     */
+    function setPermission(&$perm, $update = true)
+    {
+        $this->datatreeObject->data['perm'] = $perm->getData();
+        if ($update) {
+            return $this->datatreeObject->save();
+        }
+        return true;
+    }
+
+    /**
+     * Returns the permission of this share.
+     *
+     * @return Horde_Persm_Permission  Permission object that represents the
+     *                           permissions on this share
+     */
+    function &getPermission()
+    {
+        $perm = new Horde_Perms_Permission($this->datatreeObject->getName());
+        $perm->data = isset($this->datatreeObject->data['perm'])
+            ? $this->datatreeObject->data['perm']
+            : array();
+
+        return $perm;
+    }
+
+}
+
+/**
+ * Extension of the DataTreeObject class for storing Share information in the
+ * DataTree driver. If you want to store specialized Share information, you
+ * should extend this class instead of extending DataTreeObject directly.
+ *
+ * @author  Mike Cochrane <mike@graftonhall.co.nz>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Share
+ */
+class DataTreeObject_Share extends DataTreeObject {
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['datatree']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Maps this object's attributes from the data array into a format that we
+     * can store in the attributes storage backend.
+     *
+     * @access protected
+     *
+     * @param boolean $permsonly  Only process permissions? Lets subclasses
+     *                            override part of this method while handling
+     *                            their additional attributes seperately.
+     *
+     * @return array  The attributes array.
+     */
+    function _toAttributes($permsonly = false)
+    {
+        // Default to no attributes.
+        $attributes = array();
+
+        foreach ($this->data as $key => $value) {
+            if ($key == 'perm') {
+                foreach ($value as $type => $perms) {
+                    if (is_array($perms)) {
+                        foreach ($perms as $member => $perm) {
+                            $attributes[] = array('name' => 'perm_' . $type,
+                                                  'key' => $member,
+                                                  'value' => $perm);
+                        }
+                    } else {
+                        $attributes[] = array('name' => 'perm_' . $type,
+                                              'key' => '',
+                                              'value' => $perms);
+                    }
+                }
+            } elseif (!$permsonly) {
+                $attributes[] = array('name' => $key,
+                                      'key' => '',
+                                      'value' => $value);
+            }
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Takes in a list of attributes from the backend and maps it to our
+     * internal data array.
+     *
+     * @access protected
+     *
+     * @param array $attributes   The list of attributes from the backend
+     *                            (attribute name, key, and value).
+     * @param boolean $permsonly  Only process permissions? Lets subclasses
+     *                            override part of this method while handling
+     *                            their additional attributes seperately.
+     */
+    function _fromAttributes($attributes, $permsonly = false)
+    {
+        // Initialize data array.
+        $this->data['perm'] = array();
+
+        foreach ($attributes as $attr) {
+            if (substr($attr['name'], 0, 4) == 'perm') {
+                if (!empty($attr['key'])) {
+                    $this->data['perm'][substr($attr['name'], 5)][$attr['key']] = $attr['value'];
+                } else {
+                    $this->data['perm'][substr($attr['name'], 5)] = $attr['value'];
+                }
+            } elseif (!$permsonly) {
+                $this->data[$attr['name']] = $attr['value'];
+            }
+        }
+    }
+
+}
diff --git a/framework/Share/Share/kolab.php b/framework/Share/Share/kolab.php
new file mode 100644 (file)
index 0000000..149f02b
--- /dev/null
@@ -0,0 +1,774 @@
+<?php
+/**
+ * $Horde: framework/Share/Share/kolab.php,v 1.70 2009-12-10 19:24:08 mrubinsk Exp $
+ *
+ * @package Horde_Share
+ */
+
+/**
+ * Horde_Share_kolab:: provides the kolab backend for the horde share driver.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Stuart Binge <omicron@mighty.co.za>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_kolab extends Horde_Share {
+
+    /**
+     * Our Kolab folder list handler
+     *
+     * @var Kolab_List
+     */
+    var $_list;
+
+    /**
+     * The share type
+     *
+     * @var string
+     */
+    var $_type;
+
+    /**
+     * A marker for the validity of the list cache
+     *
+     * @var int
+     */
+    var $_listCacheValidity;
+
+    /**
+     * The session handler.
+     *
+     * @var Horde_Kolab_Session
+     */
+    private $_session;
+
+    /**
+     * Initializes the object.
+     *
+     * @throws Horde_Exception
+     */
+    function __wakeup()
+    {
+        if (empty($GLOBALS['conf']['kolab']['enabled'])) {
+            throw new Horde_Exception('You must enable the kolab settings to use the Kolab Share driver.');
+        }
+
+        $this->_type = $this->_getFolderType($this->_app);
+        if (is_a($this->_type, 'PEAR_Error')) {
+            return $this->_type;
+        }
+
+        $this->_list = $this->getSession()->getStorage();
+
+        parent::__wakeup();
+    }
+
+    /**
+     * Set the session handler.
+     *
+     * @param Horde_Kolab_Session $session The session handler.
+     *
+     * @return NULL
+     */
+    public function setSession(Horde_Kolab_Session $session)
+    {
+        $this->_session = $session;
+    }
+
+    /**
+     * Retrieve a connected kolab session.
+     *
+     * @return Horde_Kolab_Session The connected session.
+     *
+     * @throws Horde_Kolab_Session_Exception
+     */
+    public function getSession()
+    {
+        if (!isset($this->_session)) {
+            $this->_session = Horde_Kolab_Session_Singleton::singleton();
+        }
+        return $this->_session;
+    }
+
+    private function _getFolderType($app)
+    {
+        switch ($app) {
+        case 'mnemo':
+            return 'note';
+        case 'kronolith':
+            return 'event';
+        case 'turba':
+            return 'contact';
+        case 'nag':
+            return 'task';
+        default:
+            return PEAR::raiseError(sprintf(_("The Horde/Kolab integration engine does not support \"%s\""), $app));
+        }
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_sortList'], $properties['_list']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Returns a Horde_Share_Object_kolab object of the request folder.
+     *
+     * @param string $object  The share to fetch.
+     *
+     * @return Horde_Share_Object_kolab  The share object.
+     */
+    function &_getShare($object)
+    {
+        if (empty($object)) {
+            $error = PEAR::raiseError('No object requested.');
+            return $error;
+        }
+
+        /** Get the corresponding folder for this share ID */
+        $folder = $this->_list->getByShare($object, $this->_type);
+        if (is_a($folder, 'PEAR_Error')) {
+            return $folder;
+        }
+
+        /** Does the folder exist? */
+        if (!$folder->exists()) {
+            return PEAR::raiseError(sprintf(_("Share \"%s\" does not exist."), $object));
+        }
+
+        /** Create the object from the folder */
+        $share = new Horde_Share_Object_kolab($object, $this->_type);
+        $result = $share->setFolder($folder);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $share;
+    }
+
+    /**
+     * Returns a Horde_Share_Object_kolab object of the requested folder.
+     *
+     * @param string $id  The id of the share to fetch.
+     *
+     * @return Horde_Share_Object_kolab  The share object.
+     */
+    function &_getShareById($id)
+    {
+        return $this->_getShare($id);
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object_kolab objects corresponding to
+     * the requested folders.
+     *
+     * @param string $ids  The ids of the shares to fetch.
+     *
+     * @return array  An array of Horde_Share_Object_kolab objects.
+     */
+    function &_getShares($ids)
+    {
+        $objects = array();
+        foreach ($ids as $id) {
+            $result = &$this->_getShare($id);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            $objects[$id] = &$result;
+        }
+        return $objects;
+    }
+
+    /**
+     * Lists *all* shares for the current app/share, regardless of
+     * permissions.
+     *
+     * Currently not implemented in this class.
+     *
+     * @return array  All shares for the current app/share.
+     */
+    function &_listAllShares()
+    {
+        $shares = array();
+        return $shares;
+    }
+
+    /**
+     * Returns an array of all shares that $userid has access to.
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return array  The shares the user has access to.
+     */
+    function &_listShares($userid, $perm = Horde_Perms::SHOW,
+                          $attributes = null, $from = 0, $count = 0,
+                          $sort_by = null, $direction = 0)
+    {
+        $key = serialize(array($this->_type, $userid, $perm, $attributes));
+        if (empty($this->_listCache[$key])
+            || $this->_list->validity != $this->_listCacheValidity) {
+            $sharelist = $this->_list->getByType($this->_type);
+            if (is_a($sharelist, 'PEAR_Error')) {
+                return $sharelist;
+            }
+
+            $shares = array();
+            foreach ($sharelist as $folder) {
+                $id = $folder->getShareId();
+                $share = &$this->getShare($id);
+                if (is_a($share, 'PEAR_Error')) {
+                    return $share;
+                }
+
+                $keep = true;
+                if (!$share->hasPermission($userid, $perm)) {
+                    $keep = false;
+                }
+                if (isset($attributes) && $keep) {
+                    if (is_array($attributes)) {
+                        foreach ($attributes as $key => $value) {
+                            if (!$share->get($key) == $value) {
+                                $keep = false;
+                                break;
+                            }
+                        }
+                    } elseif (!$share->get('owner') == $attributes) {
+                        $keep = false;
+                    }
+                }
+                if ($keep) {
+                    $shares[] = $id;
+                }
+            }
+            $this->_listCache[$key] = $shares;
+            $this->_listCacheValidity = $this->_list->validity;
+        }
+
+        return $this->_listCache[$key];
+    }
+
+    /**
+     * Returns the number of shares that $userid has access to.
+     *
+     * @since Horde 3.2
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return integer  The number of shares
+     */
+    function _countShares($userid, $perm = Horde_Perms::SHOW,
+                          $attributes = null)
+    {
+        $shares = $this->_listShares($userid, $perm, $attributes);
+        if (is_a($share, 'PEAR_Error')) {
+            return $share;
+        }
+
+        return count($shares);
+    }
+
+    /**
+     * Returns a new share object.
+     *
+     * @param string $name  The share's name.
+     *
+     * @return Horde_Share_Object_kolab  A new share object.
+     */
+    function &_newShare($name)
+    {
+        $storageObject = new Horde_Share_Object_kolab($name, $this->_type);
+        return $storageObject;
+    }
+
+    /**
+     * Adds a share to the shares system.
+     *
+     * The share must first be created with Horde_Share_kolab::_newShare(),
+     * and have any initial details added to it, before this function is
+     * called.
+     *
+     * @param Horde_Share_Object_kolab $share  The new share object.
+     */
+    function _addShare(&$share)
+    {
+        return $share->save();
+    }
+
+    /**
+     * Removes a share from the shares system permanently.
+     *
+     * @param Horde_Share_Object_kolab $share  The share to remove.
+     */
+    function _removeShare(&$share)
+    {
+        $share_id = $share->getName();
+
+        $result = $share->delete();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+    }
+
+    /**
+     * Checks if a share exists in the system.
+     *
+     * @param string $share  The share to check.
+     *
+     * @return boolean  True if the share exists.
+     */
+    function _exists($object)
+    {
+        if (empty($object)) {
+            return false;
+        }
+
+        /** Get the corresponding folder for this share ID */
+        $folder = $this->_list->getByShare($object, $this->_type);
+        if (is_a($folder, 'PEAR_Error')) {
+            return $folder;
+        }
+
+        return $folder->exists();
+    }
+
+    /**
+     * Create a default share for the current app
+     *
+     * @return string The share ID of the new default share.
+     */
+    function getDefaultShare()
+    {
+        $default = $this->_list->getDefault($this->_type);
+        if (is_a($default, 'PEAR_Error')) {
+            return $default;
+        }
+        if ($default !== false) {
+            return $this->getShare($default->getShareId());
+        }
+
+        /** Okay, no default folder yet */
+        $share = $this->newShare(Horde_Auth::getAuth());
+        if (is_a($share, 'PEAR_Error')) {
+            return $share;
+        }
+        /** The value does not matter here as the share will rewrite it */
+        $share->set('name', '');
+        $result = $this->addShare($share);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return $share;
+    }
+}
+
+/**
+ * Extension of the Horde_Share_Object class for handling Kolab share
+ * information.
+ *
+ * @author  Stuart Binge <omicron@mighty.co.za>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_Object_kolab extends Horde_Share_Object {
+
+    /**
+     * The Kolab folder this share is based on.
+     *
+     * @var Kolab_Folder
+     */
+    var $_folder;
+
+    /**
+     * The Kolab folder name.
+     *
+     * @var string
+     */
+    var $_folder_name;
+
+    /**
+     * A cache for the share attributes.
+     *
+     * @var array
+     */
+    var $_data;
+
+    /**
+     * Our Kolab folder list handler
+     *
+     * @var Kolab_List
+     */
+    var $_list;
+
+    /**
+     * The session handler.
+     *
+     * @var Horde_Kolab_Session
+     */
+    private $_session;
+
+    /**
+     * Constructor.
+     *
+     * Sets the folder name.
+     *
+     * @param string $id  The share id.
+     */
+    function Horde_Share_Object_kolab($id, $type)
+    {
+        // We actually ignore the random id string that all horde apps provide
+        // as initial name and wait for a set('name', 'xyz') call. But we want
+        // to know if we should create a default share.
+        if ($id == Horde_Auth::getAuth()) {
+            $this->_data['default'] = true;
+        } else {
+            $this->_data['default'] = false;
+        }
+        $this->_type = $type;
+        $this->__wakeup();
+    }
+
+    /**
+     * Set the session handler.
+     *
+     * @param Horde_Kolab_Session $session The session handler.
+     *
+     * @return NULL
+     */
+    public function setSession(Horde_Kolab_Session $session)
+    {
+        $this->_session = $session;
+    }
+
+    /**
+     * Retrieve a connected kolab session.
+     *
+     * @return Horde_Kolab_Session The connected session.
+     *
+     * @throws Horde_Kolab_Session_Exception
+     */
+    public function getSession()
+    {
+        if (!isset($this->_session)) {
+            $this->_session = Horde_Kolab_Session_Singleton::singleton();
+        }
+        return $this->_session;
+    }
+
+    /**
+     * Associates a Share object with this share.
+     *
+     * @param Horde_Share $shareOb  The Share object.
+     */
+    function setShareOb(&$shareOb)
+    {
+        /** Ignore the parent as we don't need it */
+    }
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+        $this->_list = $this->getSession()->getStorage();
+        if (isset($this->_folder_name)) {
+            $this->_folder = $this->_list->getFolder($this->_folder_name);
+        }
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_shareOb'], $properties['_list'],
+              $properties['_folder']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    /**
+     * Returns the default share name for the current application.
+     *
+     * @return string  The default share name.
+     */
+    function getDefaultShareName()
+    {
+        switch ($this->_type) {
+        case 'contact':
+            return _("Contacts");
+        case 'note':
+            return _("Notes");
+        case 'event':
+            return _("Calendar");
+        case 'task':
+            return _("Tasks");
+        case 'filter':
+            return _("Filters");
+        case 'h-prefs':
+            return _("Preferences");
+        }
+    }
+
+    /**
+     * Sets the folder for this storage object.
+     *
+     * @param string $folder  Name of the Kolab folder.
+     * @param array  $perms  The permissions of the folder if they are known.
+     */
+    function setFolder(&$folder)
+    {
+        if (!isset($this->_folder)) {
+            $this->_folder = &$folder;
+            $this->_folder_name = $folder->name;
+        } else {
+            return PEAR::raiseError(_("The share has already been initialized!"));
+        }
+    }
+
+    /**
+     * Returns the ID of this share.
+     *
+     * @return string  The share's ID.
+     */
+    function _getId()
+    {
+        return $this->_folder->getShareId();
+    }
+
+    /**
+     * Returns the name of this share.
+     *
+     * @return string  The share's name.
+     */
+    function _getName()
+    {
+        return $this->_folder->getShareId();
+    }
+
+    /**
+     * Returns an attribute value from this object.
+     *
+     * @param string $attribute  The attribute to return.
+     *
+     * @return mixed  The value for $attribute.
+     */
+    function _get($attribute)
+    {
+        if (isset($this->_data[$attribute])) {
+            return $this->_data[$attribute];
+        }
+
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+
+        switch ($attribute) {
+        case 'owner':
+            $this->_data['owner'] = $this->_folder->getOwner();
+            break;
+
+        case 'name':
+            $this->_data['name'] = $this->_folder->getTitle();
+            break;
+
+        case 'type':
+            $this->_data['type'] = $this->_folder->getType();
+            break;
+
+        case 'params':
+            $params = @unserialize($this->_folder->getAttribute('params'));
+            $default = array('source' => 'kolab',
+                             'default' => $this->get('default'),
+                             'name' => $this->get('name'));
+            $type = $this->get('type');
+            if (!is_a($type, 'PEAR_Error') && $type == 'event') {
+                $default = array_merge($default, array(
+                                           'fbrelevance' => $this->_folder->getFbrelevance(),
+                                           'xfbaccess'   => $this->_folder->getXfbaccess()
+                                       ));
+            }
+            if (is_a($params, 'PEAR_Error') || $params == '') {
+                $params = $default;
+            }
+            $this->_data['params'] = serialize(array_merge($default, $params));
+            break;
+
+        case 'default':
+            $this->_data['default'] = $this->_folder->isDefault();
+            break;
+
+        default:
+            $annotation = $this->_folder->getAttribute($attribute);
+            if (is_a($annotation, 'PEAR_Error') || empty($annotation)) {
+                $annotation = '';
+            }
+            $this->_data[$attribute] = $annotation;
+            break;
+        }
+
+        return $this->_data[$attribute];
+    }
+
+    /**
+     * Sets an attribute value in this object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     *
+     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
+     *                otherwise.
+     */
+    function _set($attribute, $value)
+    {
+        switch ($attribute) {
+        case 'name':
+            /* On folder creation of default shares we wish to ignore
+             * the names provided by the Horde applications. We use
+             * the Kolab default names. */
+            if (!isset($this->_folder)) {
+                if ($this->get('default')) {
+                    $value = $this->getDefaultShareName();
+                }
+                $this->setFolder($this->_list->getNewFolder());
+            }
+            $this->_folder->setName($value);
+            $this->_data['name'] = $this->_folder->getTitle();
+            break;
+
+        case 'params':
+            $value = unserialize($value);
+            if (isset($value['default'])) {
+                $this->_data['default'] = $value['default'];
+                unset($value['default']);
+            }
+            $value = serialize($value);
+
+        default:
+            $this->_data[$attribute] = $value;
+        }
+    }
+
+    /**
+     * Saves the current attribute values.
+     */
+    function _save()
+    {
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+
+        $data = $this->_data;
+        /** The name is handled immediately when set */
+        unset($data['name']);
+        $data['type'] = $this->_type;
+
+        $result = $this->_folder->save($data);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /** The name may have changed */
+        $this->_data['name'] = $this->_folder->getTitle();
+        $this->_folder_name = $this->_folder->name;
+        return true;
+    }
+
+    /**
+     * Delete this share.
+     *
+     * @return boolean|PEAR_Error  True on success.
+     */
+    function delete()
+    {
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+        return $this->_folder->delete();
+    }
+
+    /**
+     * Checks to see if a user has a given permission.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A Horde_Perms::* constant to test for.
+     * @param string $creator      The creator of the shared object.
+     *
+     * @return boolean|PEAR_Error  Whether or not $userid has $permission.
+     */
+    function hasPermission($userid, $permission, $creator = null)
+    {
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+        return $this->_folder->hasPermission($userid, $permission, $creator);
+    }
+
+    /**
+     * Returns the permissions from this storage object.
+     *
+     * @return Horde_Perms_Permission_Kolab|PEAR_Error  The permissions on the share.
+     */
+    function &getPermission()
+    {
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+        return $this->_folder->getPermission();
+    }
+
+    /**
+     * Sets the permissions on the share.
+     *
+     * @param Horde_Perms_Permission_Kolab $perms Permission object to folder on the
+     *                                     object.
+     * @param boolean $update              Save the updated information?
+     *
+     * @return boolean|PEAR_Error  True on success.
+     */
+    function setPermission(&$perms, $update = true)
+    {
+        if (!isset($this->_folder)) {
+            return $this->_folderError();
+        }
+        return $this->_folder->setPermission($perms, $update);
+    }
+
+    /**
+     * Return a standard error in case the share has not been
+     * correctly initialized.
+     *
+     * @return PEAR_Error  The PEAR_Error to return.
+     */
+    function _folderError()
+    {
+        return PEAR::raiseError(_("The Kolab share object has not been initialized yet!"));
+    }
+}
diff --git a/framework/Share/Share/sql.php b/framework/Share/Share/sql.php
new file mode 100644 (file)
index 0000000..59f3c9d
--- /dev/null
@@ -0,0 +1,1194 @@
+<?php
+/**
+ * Horde_Share_sql:: provides the sql backend for the horde share
+ * driver.
+ *
+ * $Horde: framework/Share/Share/sql.php,v 1.76 2009/12/18 13:49:02 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Duck <duck@obala.net>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+
+/** The share has user permissions */
+define('HORDE_SHARE_SQL_FLAG_USERS', 1);
+
+/** The share has group permissions */
+define('HORDE_SHARE_SQL_FLAG_GROUPS', 2);
+
+/**
+ * @package Horde_Share
+ */
+class Horde_Share_sql extends Horde_Share {
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * SQL connection parameters
+     */
+    var $_params = array();
+
+    /**
+     * Main share table for the current scope.
+     *
+     * @var string
+     */
+    var $_table;
+
+    /**
+     * The Horde_Share_Object subclass to instantiate objects as
+     *
+     * @var string
+     */
+    var $_shareObject = 'Horde_Share_Object_sql';
+
+    /**
+     * Initializes the object.
+     */
+    function __wakeup()
+    {
+        $this->_table = $this->_app . '_shares';
+        $this->_connect();
+
+        foreach (array_keys($this->_cache) as $name) {
+            $this->_cache[$name]->setShareOb($this);
+        }
+
+        parent::__wakeup();
+    }
+
+    /**
+     * Returns the properties that need to be serialized.
+     *
+     * @return array  List of serializable properties.
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_sortList'],
+              $properties['_db'],
+              $properties['_write_db']);
+        return array_keys($properties);
+    }
+
+    /**
+     * Get storage table
+     */
+    function getTable()
+    {
+        return $this->_table;
+    }
+
+    /**
+     * Refetence to write db
+     */
+    function getWriteDb()
+    {
+        return $this->_write_db;
+    }
+
+    /**
+     * Finds out if the share has user set
+     */
+    function _hasUsers($share)
+    {
+        return $share['share_flags'] & HORDE_SHARE_SQL_FLAG_USERS;
+    }
+
+    /**
+     * Finds out if the share has user set
+     */
+    function _hasGroups($share)
+    {
+        return $share['share_flags'] & HORDE_SHARE_SQL_FLAG_GROUPS;
+    }
+
+    /**
+     * Get users permissions
+     *
+     * @param array $share Share data array
+     */
+    function _getShareUsers(&$share)
+    {
+        if ($this->_hasUsers($share)) {
+            $stmt = $this->_db->prepare('SELECT user_uid, perm FROM ' . $this->_table . '_users WHERE share_id = ?');
+            if (is_a($stmt, 'PEAR_Error')) {
+                Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $stmt;
+            }
+            $result = $stmt->execute(array($share['share_id']));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($row = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $share['perm']['users'][$row['user_uid']] = (int)$row['perm'];
+                }
+            }
+            $stmt->free();
+            $result->free();
+        }
+    }
+
+    /**
+     * Get groups permissions
+     *
+     * @param array $share Share data array
+     */
+    function _getShareGroups(&$share)
+    {
+        if ($this->_hasGroups($share)) {
+            // Get groups permissions
+            $stmt = $this->_db->prepare('SELECT group_uid, perm FROM ' . $this->_table . '_groups WHERE share_id = ?');
+            if (is_a($stmt, 'PEAR_Error')) {
+                Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $stmt;
+            }
+            $result = $stmt->execute(array($share['share_id']));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($row = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $share['perm']['groups'][$row['group_uid']] = (int)$row['perm'];
+                }
+            }
+            $stmt->free();
+            $result->free();
+        }
+    }
+
+    /**
+     * Returns a Horde_Share_Object_sql object corresponding to the given
+     * share name, with the details retrieved appropriately.
+     *
+     * @param string $name  The name of the share to retrieve.
+     *
+     * @return Horde_Share_Object_sql  The requested share.
+     */
+    function _getShare($name)
+    {
+        $stmt = $this->_db->prepare('SELECT * FROM ' . $this->_table . ' WHERE share_name = ?');
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $results = $stmt->execute(array($name));
+        if (is_a($results, 'PEAR_Error')) {
+            Horde::logMessage($results, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $results;
+        }
+        $data = $results->fetchRow(MDB2_FETCHMODE_ASSOC);
+        if (is_a($data, 'PEAR_Error')) {
+            Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $data;
+        } elseif (empty($data)) {
+            return PEAR::RaiseError(sprintf(_("Share \"%s\" does not exist."), $name));
+        }
+        $stmt->free();
+        $results->free();
+
+        // Convert charset
+        $data = $this->_fromDriverCharset($data);
+
+        // Populate the perms array
+        $this->_loadPermissions($data);
+
+        return new $this->_shareObject($data);
+    }
+
+    /**
+     * Helper function to load the permissions data into the share data
+     *
+     * @param array $data  Array of share attributes
+     */
+    function _loadPermissions(&$data)
+    {
+        $this->_getShareUsers($data);
+        $this->_getShareGroups($data);
+        $this->_getSharePerms($data);
+    }
+
+    function _getSharePerms(&$data)
+    {
+        $data['perm']['type'] = 'matrix';
+        $data['perm']['default'] = isset($data['perm_default']) ? (int)$data['perm_default'] : 0;
+        $data['perm']['guest'] = isset($data['perm_guest']) ? (int)$data['perm_guest'] : 0;
+        $data['perm']['creator'] = isset($data['perm_creator']) ? (int)$data['perm_creator'] : 0;
+        unset($data['perm_creator'], $data['perm_guest'], $data['perm_default']);
+    }
+
+    /**
+     * Returns a Horde_Share_Object_sql object corresponding to the given
+     * unique ID, with the details retrieved appropriately.
+     *
+     * @param integer $cid  The id of the share to retrieve.
+     *
+     * @return Horde_Share_Object_sql  The requested share.
+     */
+    function _getShareById($id)
+    {
+        $params = array($id);
+        $stmt = $this->_db->prepare('SELECT * FROM ' . $this->_table . ' WHERE share_id = ?');
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $results = $stmt->execute($params);
+        if (is_a($results, 'PEAR_Error')) {
+            Horde::logMessage($results, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $results;
+        }
+        $data = $results->fetchRow(MDB2_FETCHMODE_ASSOC);
+        if (is_a($data, 'PEAR_Error')) {
+            Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $data;
+        } elseif (empty($data)) {
+            return PEAR::RaiseError(sprintf(_("Share ID %d does not exist."), $id));
+        }
+
+        $stmt->free();
+        $results->free();
+
+        // Convert charset
+        $data = $this->_fromDriverCharset($data);
+
+        // Get permissions
+        $this->_loadPermissions($data);
+
+        return new $this->_shareObject($data);
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object_sql objects corresponding
+     * to the given set of unique IDs, with the details retrieved
+     * appropriately.
+     *
+     * @param array $cids  The array of ids to retrieve.
+     *
+     * @return array  The requested shares.
+     */
+    function _getShares($ids)
+    {
+        $shares = array();
+        $query = 'SELECT * FROM ' . $this->_table . ' WHERE share_id IN (' . implode(', ', $ids) . ')';
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        $groups = array();
+        $users = array();
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $shares[(int)$share['share_id']] = $this->_fromDriverCharset($share);
+            if ($this->_hasUsers($share)) {
+                $users[] = (int)$share['share_id'];
+            }
+            if ($this->_hasGroups($share)) {
+                $groups[] = (int)$share['share_id'];
+            }
+        }
+        $result->free();
+
+        // Get users permissions
+        if (!empty($users)) {
+            $query = 'SELECT share_id, user_uid, perm FROM ' . $this->_table . '_users '
+                    . ' WHERE share_id IN (' . implode(', ', $users) . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['users'][$share['user_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        // Get groups permissions
+        if (!empty($groups)) {
+            $query = 'SELECT share_id, group_uid, perm FROM ' . $this->_table . '_groups'
+                   . ' WHERE share_id IN (' . implode(', ', $groups) . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['groups'][$share['group_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        $sharelist = array();
+        foreach ($shares as $id => $data) {
+            $this->_getSharePerms($data);
+            $sharelist[$data['share_name']] = new $this->_shareObject($data);
+        }
+
+        return $sharelist;
+    }
+
+    /**
+     * Lists *all* shares for the current app/share, regardless of
+     * permissions.
+     *
+     * This is for admin functionality and scripting tools, and shouldn't be
+     * called from user-level code!
+     *
+     * @return array  All shares for the current app/share.
+     */
+    function listAllShares()
+    {
+        return $this->_listAllShares();
+    }
+
+    /**
+     * Lists *all* shares for the current app/share, regardless of
+     * permissions.
+     *
+     * @return array  All shares for the current app/share.
+     */
+    function _listAllShares()
+    {
+        $shares = array();
+        $query = 'SELECT * FROM ' . $this->_table . ' ORDER BY share_name ASC';
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $shares[(int)$share['share_id']] = $this->_fromDriverCharset($share);
+        }
+        $result->free();
+
+        // Get users permissions
+        $query = 'SELECT share_id, user_uid, perm FROM ' . $this->_table . '_users';
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (!empty($result)) {
+            while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                $shares[$share['share_id']]['perm']['users'][$share['user_uid']] = (int)$share['perm'];
+            }
+            $result->free();
+        }
+
+        // Get groups permissions
+        $query = 'SELECT share_id, group_uid, perm FROM ' . $this->_table . '_groups';
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (!empty($result)) {
+            while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                $shares[$share['share_id']]['perm']['groups'][$share['group_uid']] = (int)$share['perm'];
+            }
+            $result->free();
+        }
+
+        $sharelist = array();
+        foreach ($shares as $id => $data) {
+            $this->_getSharePerms($data);
+            $sharelist[$data['share_name']] = new $this->_shareObject($data);
+            $sharelist[$data['share_name']]->setShareOb($this);
+        }
+
+        return $sharelist;
+    }
+
+    /**
+     * Returns an array of all shares that $userid has access to.
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return array  The shares the user has access to.
+     * @throws Horde_Exception
+     */
+    function listShares($userid, $perm = Horde_Perms::SHOW, $attributes = null,
+                        $from = 0, $count = 0, $sort_by = null, $direction = 0)
+    {
+        $shares = array();
+        if (is_null($sort_by)) {
+            $sortfield = 's.share_name';
+        } elseif ($sort_by == 'owner' || $sort_by == 'id') {
+            $sortfield = 's.share_' . $sort_by;
+        } else {
+            $sortfield = 's.attribute_' . $sort_by;
+        }
+
+        $query = 'SELECT DISTINCT s.* '
+            . $this->_getShareCriteria($userid, $perm, $attributes)
+            . ' ORDER BY ' . $sortfield
+            . (($direction == 0) ? ' ASC' : ' DESC');
+        if ($from > 0 || $count > 0) {
+            $this->_db->setLimit($count, $from);
+        }
+
+        // Fix field names for sqlite. MDB2 tries to handle this with
+        // MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES, but it doesn't stick.
+        if ($this->_db->phptype == 'sqlite') {
+            $connection = $this->_db->getConnection();
+            @sqlite_query('PRAGMA full_column_names=0', $connection);
+            @sqlite_query('PRAGMA short_column_names=1', $connection);
+        }
+
+        Horde::logMessage(sprintf("SQL Query by Horde_Share_sql::listShares: %s", $query), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        $users = array();
+        $groups = array();
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $shares[(int)$share['share_id']] = $this->_fromDriverCharset($share);
+            if ($this->_hasUsers($share)) {
+                $users[] = (int)$share['share_id'];
+            }
+            if ($this->_hasGroups($share)) {
+                $groups[] = (int)$share['share_id'];
+            }
+        }
+        $result->free();
+
+        // Get users permissions
+        if (!empty($users)) {
+            $query = 'SELECT share_id, user_uid, perm FROM ' . $this->_table
+                 . '_users WHERE share_id IN (' . implode(', ', $users)
+                 . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['users'][$share['user_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        // Get groups permissions
+        if (!empty($groups)) {
+            $query = 'SELECT share_id, group_uid, perm FROM ' . $this->_table
+                     . '_groups WHERE share_id IN (' . implode(', ', $groups)
+                     . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['groups'][$share['group_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        $sharelist = array();
+        foreach ($shares as $id => $data) {
+            $this->_getSharePerms($data);
+            $sharelist[$data['share_name']] = new $this->_shareObject($data);
+            $sharelist[$data['share_name']]->setShareOb($this);
+        }
+        unset($shares);
+
+        try {
+            return Horde::callHook('share_list', array($userid, $perm, $attributes, $sharelist));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        return $sharelist;
+    }
+
+    /**
+     * Returns an array of all system shares.
+     *
+     * @return array  All system shares.
+     */
+    function listSystemShares()
+    {
+        // Fix field names for sqlite. MDB2 tries to handle this with
+        // MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES, but it doesn't stick.
+        if ($this->_db->phptype == 'sqlite') {
+            $connection = $this->_db->getConnection();
+            @sqlite_query('PRAGMA full_column_names=0', $connection);
+            @sqlite_query('PRAGMA short_column_names=1', $connection);
+        }
+
+        $query = 'SELECT * FROM ' . $this->_table . ' WHERE share_owner IS NULL';
+        Horde::logMessage('SQL Query by Horde_Share_sql::listSystemShares: ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        $sharelist = array();
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $data = $this->_fromDriverCharset($share);
+            $this->_getSharePerms($data);
+            $sharelist[$data['share_name']] = new $this->_shareObject($data);
+            $sharelist[$data['share_name']]->setShareOb($this);
+        }
+        $result->free();
+
+        return $sharelist;
+    }
+
+    /**
+     * Returns the number of shares that $userid has access to.
+     *
+     * @param string $userid     The userid of the user to check access for.
+     * @param integer $perm      The level of permissions required.
+     * @param mixed $attributes  Restrict the shares counted to those
+     *                           matching $attributes. An array of
+     *                           attribute/values pairs or a share owner
+     *                           username.
+     *
+     * @return integer  The number of shares
+     */
+    function _countShares($userid, $perm = Horde_Perms::SHOW,
+                          $attributes = null)
+    {
+        $query = $this->_getShareCriteria($userid, $perm, $attributes);
+        $query = 'SELECT COUNT(DISTINCT s.share_id) ' . $query;
+        Horde::logMessage(sprintf("SQL Query by Horde_Share_sql::_countShares: %s", $query), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $this->_db->queryOne($query);
+    }
+
+    /**
+     * Returns a new share object.
+     *
+     * @param string $name  The share's name.
+     *
+     * @return Horde_Share_Object_sql  A new share object.
+     */
+    function _newShare($name)
+    {
+        return new $this->_shareObject(array('share_name' => $name));
+    }
+
+    /**
+     * Adds a share to the shares system.
+     *
+     * The share must first be created with
+     * Horde_Share_sql::_newShare(), and have any initial details added
+     * to it, before this function is called.
+     *
+     * @param Horde_Share_Object_sql $share  The new share object.
+     */
+    function _addShare($share)
+    {
+        return $share->save();
+    }
+
+    /**
+     * Removes a share from the shares system permanently.
+     *
+     * @param Horde_Share_Object_sql $share  The share to remove.
+     */
+    function _removeShare($share)
+    {
+        $params = array($share->getId());
+        $tables = array($this->_table,
+                        $this->_table . '_users',
+                        $this->_table . '_groups');
+        foreach ($tables as $table) {
+
+            /* Remove the share entry */
+            $stmt = $this->_write_db->prepare('DELETE FROM ' . $table . ' WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
+            if (is_a($stmt, 'PEAR_Error')) {
+                Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $stmt;
+            }
+            $result = $stmt->execute($params);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            }
+            $stmt->free();
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if a share exists in the system.
+     *
+     * @param string $share  The share to check.
+     *
+     * @return boolean  True if the share exists.
+     */
+    function _exists($share)
+    {
+        $stmt = $this->_db->prepare('SELECT 1 FROM ' . $this->_table
+                . ' WHERE share_name = ?');
+
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $result = $stmt->execute(array($share));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        $exists = (bool)$result->fetchOne();
+        $stmt->free();
+        $result->free();
+
+        return $exists;
+    }
+
+    /**
+     * Returns an array of criteria for querying shares.
+     * @access protected
+     *
+     * @param string  $userid      The userid of the user to check access for.
+     * @param integer $perm        The level of permissions required.
+     * @param mixed   $attributes  Restrict the shares returned to those who
+     *                             have these attribute values.
+     *
+     * @return string  The criteria string for fetching this user's shares.
+     */
+    function _getShareCriteria($userid, $perm = Horde_Perms::SHOW,
+                               $attributes = null)
+    {
+        $query = ' FROM ' . $this->_table . ' s ';
+        $where = '';
+
+        if (!empty($userid)) {
+            // (owner == $userid)
+            $where .= 's.share_owner = ' . $this->_write_db->quote($userid);
+
+            // (name == perm_creator and val & $perm)
+            $where .= ' OR (' . Horde_SQL::buildClause($this->_db, 's.perm_creator', '&', $perm) . ')';
+
+            // (name == perm_creator and val & $perm)
+            $where .= ' OR (' . Horde_SQL::buildClause($this->_db, 's.perm_default',  '&', $perm) . ')';
+
+            // (name == perm_users and key == $userid and val & $perm)
+            $query .= ' LEFT JOIN ' . $this->_table . '_users AS u ON u.share_id = s.share_id';
+             $where .= ' OR ( u.user_uid = ' .  $this->_write_db->quote($userid)
+            . ' AND (' . Horde_SQL::buildClause($this->_db, 'u.perm', '&', $perm) . '))';
+
+            // If the user has any group memberships, check for those also.
+            require_once 'Horde/Group.php';
+            $group = Group::singleton();
+            $groups = $group->getGroupMemberships($userid, true);
+            if (!is_a($groups, 'PEAR_Error') && $groups) {
+                // (name == perm_groups and key in ($groups) and val & $perm)
+                $ids = array_keys($groups);
+                $group_ids = array();
+                foreach ($ids as $id) {
+                    $group_ids[] = $this->_db->quote($id);
+                }
+                $query .= ' LEFT JOIN ' . $this->_table . '_groups AS g ON g.share_id = s.share_id';
+                $where .= ' OR (g.group_uid IN (' . implode(',', $group_ids) . ')'
+                    . ' AND (' . Horde_SQL::buildClause($this->_db, 'g.perm', '&', $perm) . '))';
+            } elseif (is_a($groups, 'PEAR_Error')) {
+                Horde::logMessage($groups, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        } else {
+            $where = '(' . Horde_SQL::buildClause($this->_db, 's.perm_guest', '&', $perm) . ')';
+        }
+
+        $attributes = $this->_toDriverKeys($attributes);
+        $attributes = $this->_toDriverCharset($attributes);
+
+        if (is_array($attributes)) {
+            // Build attribute/key filter.
+            $where = ' (' . $where . ') ';
+            foreach ($attributes as $key => $value) {
+                $where .= ' AND ' . $key . ' = ' . $this->_db->quote($value);
+            }
+        } elseif (!empty($attributes)) {
+            // Restrict to shares owned by the user specified in the
+            // $attributes string.
+            $where = ' (' . $where . ') AND s.share_owner = ' . $this->_db->quote($attributes);
+        }
+
+        return $query . ' WHERE ' . $where;
+    }
+
+    /**
+     * Resets the current database name so that MDB2 is always selecting the
+     * database before sending a query.
+     */
+    function _selectDB($db, $scope, $message, $is_manip = null)
+    {
+        if ($scope == 'query') {
+            $db->connected_database_name = '';
+        }
+    }
+
+    /**
+     * Attempts to open a connection to the sql server.
+     *
+     * @return boolean  True on success.
+     * @throws Horde_Exception
+     */
+    function _connect()
+    {
+        $this->_params = $GLOBALS['conf']['sql'];
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $_params['username'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+
+        /* Connect to the sql server using the supplied parameters. */
+        $params = $this->_params;
+        unset($params['charset']);
+        $this->_write_db = MDB2::factory($params);
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            throw new Horde_Exception($this->_write_db);
+        }
+
+        /* Attach debug handler. */
+        $this->_write_db->setOption('debug', true);
+        $this->_write_db->setOption('debug_handler', array($this, '_selectDB'));
+        $this->_write_db->setOption('seqcol_name', 'id');
+
+        /* Set DB portability options. */
+        switch ($this->_write_db->phptype) {
+        case 'mssql':
+            $this->_write_db->setOption('field_case', CASE_LOWER);
+            $this->_write_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_RTRIM | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES);
+            break;
+
+        case 'pgsql':
+            /* The debug handler breaks PostgreSQL. In most cases it shouldn't
+             * be necessary, but this may mean we simply can't support use of
+             * multiple Postgres databases right now. See
+             * http://bugs.horde.org/ticket/7825 */
+            $this->_write_db->setOption('debug', false);
+            // Fall through
+
+        default:
+            $this->_write_db->setOption('field_case', CASE_LOWER);
+            $this->_write_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES);
+        }
+
+        /* Check if we need to set up the read DB connection seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($params, $this->_params['read']);
+            unset($params['charset']);
+            $this->_db = MDB2::singleton($params);
+            if (is_a($this->_db, 'PEAR_Error')) {
+                throw new Horde_Exception($this->_db);
+            }
+
+            $this->_db->setOption('seqcol_name', 'id');
+            /* Set DB portability options. */
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('field_case', CASE_LOWER);
+                $this->_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_RTRIM | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES);
+                break;
+
+            case 'pgsql':
+                /* The debug handler breaks PostgreSQL. In most cases it shouldn't
+                 * be necessary, but this may mean we simply can't support use of
+                 * multiple Postgres databases right now. See
+                 * http://bugs.horde.org/ticket/7825 */
+                $this->_write_db->setOption('debug', false);
+                // Fall through
+
+            default:
+                $this->_db->setOption('field_case', CASE_LOWER);
+                $this->_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES);
+            }
+        } else {
+            /* Default to the same DB handle as the writer for reading too */
+            $this->_db = $this->_write_db;
+        }
+
+        return true;
+    }
+
+    /**
+     * Utility function to convert from the SQL server's charset.
+     */
+    function _fromDriverCharset($data)
+    {
+        foreach ($data as $key => $value) {
+            if (substr($key, 0, 9) == 'attribute') {
+                $data[$key] = Horde_String::convertCharset(
+                    $data[$key], $this->_params['charset']);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Utility function to convert TO the SQL server's charset.
+     */
+    function _toDriverCharset($data)
+    {
+        if (!is_array($data)) {
+            return $data;
+        }
+
+        foreach ($data as $key => $value) {
+            if (substr($key, 0, 9) == 'attribute') {
+                $data[$key] = Horde_String::convertCharset(
+                    $data[$key], Horde_Nls::getCharset(), $this->_params['charset']);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Convert an array keyed on client keys to an array keyed on the driver
+     * keys.
+     *
+     * @param array  $data  The client code keyed array.
+     *
+     * @return array  The driver keyed array.
+     */
+    function _toDriverKeys($data)
+    {
+        if (!is_array($data)) {
+            return $data;
+        }
+
+        $driver_keys = array();
+        foreach ($data as $key => $value) {
+            if ($key == 'owner') {
+                $driver_keys['share_owner'] = $value;
+            } else {
+                $driver_keys['attribute_' . $key] = $value;
+            }
+        }
+
+        return $driver_keys;
+    }
+
+}
+
+/**
+ * Extension of the Horde_Share_Object class for storing share information in
+ * the sql driver.
+ *
+ * @author  Duck <duck@obala.net>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_Object_sql extends Horde_Share_Object {
+
+    /**
+     * The actual storage object that holds the data.
+     *
+     * @var mixed
+     */
+    var $data = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $data Share data array.
+     */
+    function Horde_Share_Object_sql($data)
+    {
+        if (!isset($data['perm']) || !is_array($data['perm'])) {
+            $this->data['perm'] = array(
+                'users' => array(),
+                'type' => 'matrix',
+                'default' => isset($data['perm_default'])
+                    ? (int)$data['perm_default'] : 0,
+                'guest' => isset($data['perm_guest'])
+                    ? (int)$data['perm_guest'] : 0,
+                'creator' => isset($data['perm_creator'])
+                    ? (int)$data['perm_creator'] : 0,
+                'groups' => array());
+
+            unset($data['perm_creator'], $data['perm_guest'],
+                  $data['perm_default']);
+        }
+        $this->data = array_merge($data, $this->data);
+    }
+
+    /**
+     * Sets an attribute value in this object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     *
+     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
+     *                otherwise.
+     */
+    function _set($attribute, $value)
+    {
+        if ($attribute == 'owner') {
+            return $this->data['share_owner'] = $value;
+        } else {
+            return $this->data['attribute_' . $attribute] = $value;
+        }
+    }
+
+    /**
+     * Returns one of the attributes of the object, or null if it isn't
+     * defined.
+     *
+     * @param string $attribute  The attribute to retrieve.
+     *
+     * @return mixed  The value of the attribute, or an empty string.
+     */
+    function _get($attribute)
+    {
+        if ($attribute == 'owner') {
+            return $this->data['share_owner'];
+        } elseif (isset($this->data['attribute_' . $attribute])) {
+            return $this->data['attribute_' . $attribute];
+        }
+    }
+
+    /**
+     * Returns the ID of this share.
+     *
+     * @return string  The share's ID.
+     */
+    function _getId()
+    {
+        return isset($this->data['share_id']) ? $this->data['share_id'] : null;
+    }
+
+    /**
+     * Returns the name of this share.
+     *
+     * @return string  The share's name.
+     */
+    function _getName()
+    {
+        return $this->data['share_name'];
+    }
+
+    /**
+     * Saves the current attribute values.
+     */
+    function _save()
+    {
+        $db = $this->_shareOb->getWriteDb();
+        $table = $this->_shareOb->getTable();
+
+        $fields = array();
+        $params = array();
+
+        foreach ($this->_shareOb->_toDriverCharset($this->data) as $key => $value) {
+            if ($key != 'share_id' && $key != 'perm' && $key != 'share_flags') {
+                $fields[] = $key;
+                $params[] = $value;
+            }
+        }
+
+        $fields[] = 'perm_creator';
+        $params[] = isset($this->data['perm']['creator']) ? (int)$this->data['perm']['creator'] : 0;
+
+        $fields[] = 'perm_default';
+        $params[] = isset($this->data['perm']['default']) ? (int)$this->data['perm']['default'] : 0;
+
+        $fields[] = 'perm_guest';
+        $params[] = isset($this->data['perm']['guest']) ? (int)$this->data['perm']['guest'] : 0;
+
+        $fields[] = 'share_flags';
+        $flags = 0;
+        if (!empty($this->data['perm']['users'])) {
+            $flags |= HORDE_SHARE_SQL_FLAG_USERS;
+        }
+        if (!empty($this->data['perm']['groups'])) {
+            $flags |= HORDE_SHARE_SQL_FLAG_GROUPS;
+        }
+        $params[] = $flags;
+
+        if (empty($this->data['share_id'])) {
+            $share_id = $db->nextId($table);
+            if (is_a($share_id, 'PEAR_Error')) {
+                Horde::logMessage($share_id, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $share_id;
+            }
+
+            $this->data['share_id'] = $share_id;
+            $fields[] = 'share_id';
+            $params[] = $this->data['share_id'];
+
+            $query = 'INSERT INTO ' . $table . ' (' . implode(', ', $fields) . ') VALUES (?' . str_repeat(', ?', count($fields) - 1) . ')';
+        } else {
+            $query = 'UPDATE ' . $table . ' SET ' . implode(' = ?, ', $fields) . ' = ? WHERE share_id = ?';
+            $params[] = $this->data['share_id'];
+        }
+        $stmt = $db->prepare($query, null, MDB2_PREPARE_MANIP);
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $result = $stmt->execute($params);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+        $stmt->free();
+
+        // Update the share's user permissions
+        $stmt = $db->prepare('DELETE FROM ' . $table . '_users WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $result = $stmt->execute(array($this->data['share_id']));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+        $stmt->free();
+
+        if (!empty($this->data['perm']['users'])) {
+            $data = array();
+            foreach ($this->data['perm']['users'] as $user => $perm) {
+                $stmt = $db->prepare('INSERT INTO ' . $table . '_users (share_id, user_uid, perm) VALUES (?, ?, ?)', null, MDB2_PREPARE_MANIP);
+                if (is_a($stmt, 'PEAR_Error')) {
+                    Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return $stmt;
+                }
+                $result = $stmt->execute(array($this->data['share_id'], $user, $perm));
+                if (is_a($result, 'PEAR_Error')) {
+                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return $result;
+                }
+                $stmt->free();
+            }
+        }
+
+        // Update the share's group permissions
+        $stmt = $db->prepare('DELETE FROM ' . $table . '_groups WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $stmt;
+        }
+        $result = $stmt->execute(array($this->data['share_id']));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+        $stmt->free();
+
+        if (!empty($this->data['perm']['groups'])) {
+            $data = array();
+            foreach ($this->data['perm']['groups'] as $group => $perm) {
+                $stmt = $db->prepare('INSERT INTO ' . $table . '_groups (share_id, group_uid, perm) VALUES (?, ?, ?)', null, MDB2_PREPARE_MANIP);
+                if (is_a($stmt, 'PEAR_Error')) {
+                    Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return $stmt;
+                }
+                $result = $stmt->execute(array($this->data['share_id'], $group, $perm));
+                if (is_a($result, 'PEAR_Error')) {
+                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return $result;
+                }
+                $stmt->free();
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks to see if a user has a given permission.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A Horde_Perms::* constant to test for.
+     * @param string $creator      The creator of the event.
+     *
+     * @return boolean  Whether or not $userid has $permission.
+     */
+    function hasPermission($userid, $permission, $creator = null)
+    {
+        if ($userid == $this->data['share_owner']) {
+            return true;
+        }
+
+        return $GLOBALS['perms']->hasPermission($this->getPermission(),
+                                                $userid, $permission, $creator);
+    }
+
+    /**
+     * Sets the permission of this share.
+     *
+     * @param Horde_Perms_Permission $perm  Permission object.
+     * @param boolean $update               Should the share be saved
+     *                                      after this operation?
+     *
+     * @return boolean  True if no error occured, PEAR_Error otherwise
+     */
+    function setPermission($perm, $update = true)
+    {
+        $this->data['perm'] = $perm->getData();
+        if ($update) {
+            return $this->save();
+        }
+        return true;
+    }
+
+    /**
+     * Returns the permission of this share.
+     *
+     * @return Horde_Perms_Permission  Permission object that represents the
+     *                                 permissions on this share.
+     */
+    function getPermission()
+    {
+        $perm = new Horde_Perms_Permission($this->getName());
+        $perm->data = isset($this->data['perm'])
+            ? $this->data['perm']
+            : array();
+
+        return $perm;
+    }
+
+}
diff --git a/framework/Share/Share/sql_hierarchical.php b/framework/Share/Share/sql_hierarchical.php
new file mode 100644 (file)
index 0000000..16acc15
--- /dev/null
@@ -0,0 +1,757 @@
+<?php
+/**
+ * Implementation of Horde_Share class for shared objects that are hierarchical
+ * in nature.
+ *
+ * $Horde: framework/Share/Share/sql_hierarchical.php,v 1.56 2009-12-10 19:24:08 mrubinsk Exp $
+ *
+ * @author  Duck <duck@obala.net>
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @since   Horde 3.2
+ * @package Horde_Share
+ */
+class Horde_Share_sql_hierarchical extends Horde_Share_sql {
+
+    /**
+     * The Horde_Share_Object subclass to instantiate objects as
+     *
+     * @var string
+     */
+    var $_shareObject = 'Horde_Share_Object_sql_hierarchical';
+
+    /**
+     * Override new share creation so we can allow for shares with empty
+     * share_names.
+     *
+     */
+    function &newShare($name = '')
+    {
+        $share = &$this->_newShare();
+        $share->setShareOb($this);
+        $share->set('owner', Horde_Auth::getAuth());
+        return $share;
+    }
+
+    /**
+     * Returns a new share object.
+     *
+     * @param string $name  The share's name.
+     *
+     * @return Horde_Share_Object_sql  A new share object.
+     */
+    function &_newShare()
+    {
+        $share = new $this->_shareObject();
+        return $share;
+    }
+
+    /**
+     * Returns an array of all shares that $userid has access to.
+     *
+     * @param string $userid      The userid of the user to check access for.
+     * @param integer $perm       The level of permissions required.
+     * @param mixed $attributes   Restrict the shares counted to those
+     *                            matching $attributes. An array of
+     *                            attribute/values pairs or a share owner
+     *                            username.
+     * @param integer $from       The share to start listing from.
+     * @param integer $count      The number of shares to return.
+     * @param string $sort_by     The field to sort by
+     * @param integer $direction  The sort direction
+     * @param mixed $parent       Either a share_id, Horde_Share_Object or null.
+     * @param boolean $alllevels  List all levels or just the direct children
+     *                            of $parent?
+     *
+     * @return mixed  The shares the user has access to || PEAR_Error
+     */
+    function &listShares($userid, $perm = Horde_Perms::SHOW, $attributes = null,
+                         $from = 0,  $count = 0, $sort_by = null,
+                         $direction = 0, $parent = null,
+                         $allLevels = true, $ignorePerms = false)
+    {
+        $shares = array();
+        if (is_null($sort_by)) {
+            $sortfield = 's.share_id';
+        } elseif ($sort_by == 'owner' || $sort_by == 'id') {
+            $sortfield = 's.share_' . $sort_by;
+        } else {
+            $sortfield = 's.attribute_' . $sort_by;
+        }
+
+        $query = 'SELECT DISTINCT s.* '
+                 . $this->_getShareCriteria($userid, $perm, $attributes,
+                                            $parent, $allLevels, $ignorePerms)
+                 . ' ORDER BY ' . $sortfield
+                 . (($direction == 0) ? ' ASC' : ' DESC');
+        if ($from > 0 || $count > 0) {
+            $this->_db->setLimit($count, $from);
+        }
+
+        Horde::logMessage('Query By Horde_Share_sql_hierarchical: ' . $query,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        $users = array();
+        $groups = array();
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $shares[(int)$share['share_id']] = $this->_fromDriverCharset($share);
+            if ($this->_hasUsers($share)) {
+                $users[] = (int)$share['share_id'];
+            }
+            if ($this->_hasGroups($share)) {
+                $groups[] = (int)$share['share_id'];
+            }
+        }
+        $result->free();
+
+        // Get users permissions
+        if (!empty($users)) {
+            $query = 'SELECT share_id, user_uid, perm FROM ' . $this->_table
+                     . '_users WHERE share_id IN (' . implode(', ', $users)
+                     . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['users'][$share['user_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        // Get groups permissions
+        if (!empty($groups)) {
+            $query = 'SELECT share_id, group_uid, perm FROM ' . $this->_table
+                     . '_groups WHERE share_id IN (' . implode(', ', $groups)
+                     . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['groups'][$share['group_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        $sharelist = array();
+        foreach ($shares as $id => $data) {
+            $this->_getSharePerms($data);
+            $sharelist[$id] = new $this->_shareObject($data);
+            $sharelist[$id]->setShareOb($this);
+        }
+        unset($shares);
+
+        try {
+            return Horde::callHook('share_list', array($userid, $perm, $attributes, $sharelist));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        return $sharelist;
+    }
+
+    /**
+     * Returns an array of criteria for querying shares.
+     * @access protected
+     *
+     * @param string $userid      The userid of the user to check access for.
+     * @param integer $perm       The level of permissions required.
+     * @param mixed $attributes   Restrict the shares returned to those who
+     *                            have these attribute values.
+     * @param mixed $parent       The share to start searching in.
+     *                            (A Horde_Share_Object, share_id or null)
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent? Defaults to all levels.
+     *
+     * @return string  The criteria string for fetching this user's shares.
+     */
+    function _getShareCriteria($userid, $perm = Horde_Perms::SHOW, $attributes = null,
+                               $parent = null, $allLevels = true,
+                               $ignorePerms = false)
+    {
+        static $criteria;
+
+        if (is_a($parent, 'Horde_Share_Object')) {
+            $parent_id = $parent->getId();
+        } else {
+            $parent_id = $parent;
+        }
+        $key = $userid . $perm . $parent_id . $allLevels
+               . (is_array($attributes) ? serialize($attributes) : $attributes);
+        if (isset($criteria[$key])) {
+            return $criteria[$key];
+        }
+
+        $query = ' FROM ' . $this->_table . ' s ';
+        $where = '';
+
+        if (!$ignorePerms) {
+            if (empty($userid)) {
+                $where = '(' . Horde_SQL::buildClause($this->_db, 's.perm_guest', '&', $perm) . ')';
+            } else {
+                // (owner == $userid)
+                $where = 's.share_owner = ' . $this->_db->quote($userid);
+
+                // (name == perm_creator and val & $perm)
+                $where .= ' OR (' . Horde_SQL::buildClause($this->_db, 's.perm_creator', '&', $perm) . ')';
+
+                // (name == perm_creator and val & $perm)
+                $where .= ' OR (' . Horde_SQL::buildClause($this->_db, 's.perm_default',  '&', $perm) . ')';
+
+                // (name == perm_users and key == $userid and val & $perm)
+                $query .= ' LEFT JOIN ' . $this->_table . '_users AS u ON u.share_id = s.share_id';
+                $where .= ' OR ( u.user_uid = ' .  $this->_write_db->quote($userid)
+                . ' AND (' . Horde_SQL::buildClause($this->_db, 'u.perm', '&', $perm) . '))';
+
+                // If the user has any group memberships, check for those also.
+                require_once 'Horde/Group.php';
+                $group = &Group::singleton();
+                $groups = $group->getGroupMemberships($userid, true);
+                if (!is_a($groups, 'PEAR_Error') && $groups) {
+                    // (name == perm_groups and key in ($groups) and val & $perm)
+                    $ids = array_keys($groups);
+                    $group_ids = array();
+                    foreach ($ids as $id) {
+                        $group_ids[] = $this->_db->quote($id);
+                    }
+                    $query .= ' LEFT JOIN ' . $this->_table . '_groups AS g ON g.share_id = s.share_id';
+                    $where .= ' OR (g.group_uid IN (' . implode(',', $group_ids) . ')'
+                        . ' AND (' . Horde_SQL::buildClause($this->_db, 'g.perm', '&', $perm) . '))';
+                }
+            }
+        }
+
+        /* Convert to driver's keys */
+        $attributes = $this->_toDriverKeys($attributes);
+
+        /* ...and to driver charset */
+        $attributes = $this->_toDriverCharset($attributes);
+
+        if (is_array($attributes)) {
+            // Build attribute/key filter.
+            if (!empty($where)) {
+                $where = ' (' . $where . ') ';
+            }
+            foreach ($attributes as $key => $value) {
+                $where .= ' AND ' . $key;
+                if (is_array($value)) {
+                    $where .= ' ' . $value[0]. ' ' . $this->_db->quote($value[1]);
+                } else {
+                    $where .= ' = ' . $this->_db->quote($value);
+                }
+            }
+        } elseif (!empty($attributes)) {
+            // Restrict to shares owned by the user specified
+            $where = (!empty($where) ? ' (' . $where . ') AND ' : ' ') . 's.share_owner = ' . $this->_db->quote($attributes);
+        }
+
+        // See if we need to filter by parent or get the parent object
+        if ($parent != null) {
+            if (!is_a($parent, 'Horde_Share_Object')) {
+                $parent = $this->getShareById($parent);
+                if (is_a($parent, 'PEAR_Error')) {
+                    return $parent;
+                }
+            }
+
+            // Need to append the parent's share id to the list of parents in
+            // order to search the share_parents field.
+            $parents = $parent->get('parents') . ':' . $parent->getId();
+            if ($allLevels) {
+                $where_parent = '(share_parents = ' . $this->_db->quote($parents)
+                        . ' OR share_parents LIKE ' . $this->_db->quote($parents . ':%') . ')';
+            } else {
+                $where_parent = 's.share_parents = ' . $this->_db->quote($parents);
+            }
+        } elseif (!$allLevels) {
+            // No parents, and we only want the root.
+            $where_parent = "(s.share_parents = '' OR s.share_parents IS NULL)";
+        }
+
+        if (empty($where_parent)) {
+            $criteria[$key] = $query . ' WHERE ' . $where;
+        } else {
+            if (!empty($where)) {
+                $criteria[$key] = $query . ' WHERE (' . $where . ') AND ' . $where_parent;
+            } else {
+                $criteria[$key] = $query . ' WHERE ' . $where_parent;
+            }
+        }
+
+        return $criteria[$key];
+    }
+
+    /**
+     * Return a list of users who have shares with the given permissions
+     * for the current user.
+     *
+     * @param integer $perm       The level of permissions required.
+     * @param mixed  $parent      The parent share to start looking in.
+     *                            (Horde_Share_Object, share_id, or null)
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent? Defaults to all levels.
+     * @param integer $from       The user to start listing at.
+     * @param integer $count      The number of users to return.
+     *
+     * @return array  List of users.
+     */
+    function listOwners($perm = Horde_Perms::SHOW, $parent = null, $allLevels = true,
+                        $from = 0, $count = 0)
+    {
+        $sql = 'SELECT DISTINCT(s.share_owner) '
+                . $this->_getShareCriteria(Horde_Auth::getAuth(), $perm, null,
+                                           $parent, $allLevels);
+
+        if ($count) {
+            $this->_db->setLimit($count, $from);
+        }
+
+        $allowners = $this->_db->queryCol($sql);
+        if (is_a($allowners, 'PEAR_Error')) {
+             Horde::logMessage($allowners, __FILE__, __LINE__, PEAR_LOG_ERR);
+             return $allowners;
+        }
+
+        $owners = array();
+        foreach ($allowners as $owner) {
+            if ($this->countShares(Horde_Auth::getAuth(), $perm, $owner, $parent,
+                                   $allLevels)) {
+
+                $owners[] = $owner;
+            }
+        }
+
+        return $owners;
+    }
+
+    /**
+     * Count the number of users who have shares with the given permissions
+     * for the current user.
+     *
+     * @param integer $perm       The level of permissions required.
+     * @param mixed $parent       The parent share to start looking in.
+     *                            (Horde_Share_Object, share_id, or null).
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent?
+     *
+     * @return integer  Number of users.
+     */
+    function countOwners($perm = Horde_Perms::SHOW, $parent = null, $allLevels = true)
+    {
+        $sql = 'SELECT COUNT(DISTINCT(s.share_owner)) '
+               . $this->_getShareCriteria(Horde_Auth::getAuth(), $perm, null, $parent,
+                                          $allLevels);
+
+        return $this->_db->queryOne($sql);
+    }
+
+    /**
+     * Returns a share's direct parent object.
+     *
+     * @param Horde_Share_Object $share  The share to get parent for.
+     *
+     * @return Horde_Share_Object The parent share, if it exists.
+     */
+    function getParent($child)
+    {
+        $parents = $child->get('parents');
+
+        // No parents, this is at the root.
+        if (empty($parents)) {
+            return null;
+        }
+        $parents = explode(':', $parents);
+        return $this->getShareById(array_pop($parents));
+    }
+
+    /**
+     * Returns a Horde_Share_Object object corresponding to the given unique
+     * ID, with the details retrieved appropriately.
+     *
+     * @param string $cid  The id of the share to retrieve.
+     *
+     * @return Horde_Share_Object  The requested share.
+     */
+    function &getShareById($cid)
+    {
+        if (!isset($this->_cache[$cid])) {
+            $share = &$this->_getShareById($cid);
+            if (is_a($share, 'PEAR_Error')) {
+                return $share;
+            }
+            $share->setShareOb($this);
+            $this->_cache[$cid] = &$share;
+        }
+
+        return $this->_cache[$cid];
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object objects corresponding to the
+     * given set of unique IDs, with the details retrieved appropriately.
+     *
+     * @param array $cids  The array of ids to retrieve.
+     *
+     * @return array  The requested shares keyed by share_id.
+     */
+    function &getShares($cids)
+    {
+        $all_shares = array();
+        $missing_ids = array();
+        foreach ($cids as $cid) {
+            if (isset($this->_cache[$cid])) {
+                $all_shares[] = &$this->_cache[$cid];
+            } else {
+                $missing_ids[] = $cid;
+            }
+        }
+
+        if (count($missing_ids)) {
+            $shares = &$this->_getShares($missing_ids);
+            if (is_a($shares, 'PEAR_Error')) {
+                return $shares;
+            }
+
+            foreach (array_keys($shares) as $key) {
+                $this->_cache[$key] = &$shares[$key];
+                $this->_cache[$key]->setShareOb($this);
+                $all_shares[$key] = &$this->_cache[$key];
+            }
+        }
+
+        return $all_shares;
+    }
+
+   /**
+     * Removes a share from the shares system permanently. This will recursively
+     * delete all child shares as well.
+     *
+     * @param Horde_Share_Object $share  The share to remove.
+     * @throws Horde_Exception
+     */
+    function removeShare(&$share)
+    {
+        if (!is_a($share, 'Horde_Share_Object')) {
+            return PEAR::raiseError('Shares must be Horde_Share_Object objects or extend that class.');
+        }
+
+        try {
+            Horde::callHook('share_remove', array($share));
+        } catch (Horde_Exception_HookNotSet $e) {}
+
+        /* Get the list of all $share's children */
+        $children = $share->getChildren(null, true);
+
+        /* Remove share from the caches. */
+        $id = $share->getId();
+        $this->_cache = array();
+        $this->_listCache = array();
+
+        foreach ($children as $child) {
+            $result = $this->_removeShare($child);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return $this->_removeShare($share);
+    }
+
+    /**
+     * Returns an array of Horde_Share_Object_sql objects corresponding
+     * to the given set of unique IDs, with the details retrieved
+     * appropriately.
+     *
+     * @param array $cids  The array of ids to retrieve.
+     *
+     * @return array  The requested shares keyed by share_id.
+     */
+    function &_getShares($ids)
+    {
+        $shares = array();
+        $query = 'SELECT * FROM ' . $this->_table . ' WHERE share_id IN (' . implode(', ', $ids) . ')';
+        $result = $this->_db->query($query);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        } elseif (empty($result)) {
+            return array();
+        }
+
+        $groups = array();
+        $users = array();
+        while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $shares[(int)$share['share_id']] = $this->_fromDriverCharset($share);
+            if ($this->_hasUsers($share)) {
+                $users[] = (int)$share['share_id'];
+            }
+            if ($this->_hasGroups($share)) {
+                $groups[] = (int)$share['share_id'];
+            }
+        }
+        $result->free();
+
+        // Get users permissions
+        if (!empty($users)) {
+            $query = 'SELECT share_id, user_uid, perm FROM ' . $this->_table . '_users '
+                    . ' WHERE share_id IN (' . implode(', ', $users) . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['users'][$share['user_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        // Get groups permissions
+        if (!empty($groups)) {
+            $query = 'SELECT share_id, group_uid, perm FROM ' . $this->_table . '_groups'
+                   . ' WHERE share_id IN (' . implode(', ', $groups) . ')';
+            $result = $this->_db->query($query);
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            } elseif (!empty($result)) {
+                while ($share = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                    $shares[$share['share_id']]['perm']['groups'][$share['group_uid']] = (int)$share['perm'];
+                }
+                $result->free();
+            }
+        }
+
+        $sharelist = array();
+        foreach ($shares as $id => $data) {
+            $sharelist[$id] = new $this->_shareObject($data);
+        }
+
+        return $sharelist;
+    }
+
+    /**
+     * Override the Horde_Share base class to avoid any confusion
+     *
+     */
+    function getShare($name)
+    {
+        return PEAR::raiseError(_("Share names are not supported in this driver"));
+    }
+
+    /**
+     * Returns the count of all shares that $userid has access to.
+     *
+     * @param string  $userid      The userid of the user to check access for.
+     * @param integer $perm        The level of permissions required.
+     * @param mixed   $attributes  Restrict the shares counted to those
+     *                             matching $attributes. An array of
+     *                             attribute/values pairs or a share owner
+     *                             username.
+     * @param mixed  $parent      The share to start searching from
+     *                            (Horde_Share_Object, share_id, or null)
+     * @param boolean $allLevels  Return all levels, or just the direct
+     *                            children of $parent?
+     *
+     * @return integer  Number of shares the user has access to.
+     */
+    function countShares($userid, $perm = Horde_Perms::SHOW, $attributes = null,
+                         $parent = null, $allLevels = true)
+    {
+        $query = 'SELECT COUNT(DISTINCT s.share_id) '
+                 . $this->_getShareCriteria($userid, $perm, $attributes,
+                                            $parent, $allLevels);
+
+        return $this->_db->queryOne($query);
+    }
+
+}
+
+/**
+ * Class for storing Share information.
+ */
+class Horde_Share_Object_sql_hierarchical extends Horde_Share_Object_sql {
+
+    /**
+     * Constructor. This is here primarily to make calling the parent
+     * constructor(s) from any subclasses cleaner.
+     *
+     * @param unknown_type $data
+     * @return Horde_Share_Object_sql_hierarchical
+     */
+    function Horde_Share_Object_sql_hierarchical($data)
+    {
+        if (!isset($data['share_parents'])) {
+            $data['share_parents'] = null;
+        }
+        parent::Horde_Share_Object_sql($data);
+    }
+
+    function inheritPermissions()
+    {
+        //FIXME: Not called from anywhere yet anyway.
+    }
+
+    /**
+     * Return a count of the number of children this share has
+     *
+     * @param integer $perm  A Horde_Perms::* constant
+     * @param boolean $allLevels  Count grandchildren or just children
+     *
+     * @return mixed  The number of child shares || PEAR_Error
+     */
+    function countChildren($perm = Horde_Perms::SHOW, $allLevels = true)
+    {
+        return $this->_shareOb->countShares(Horde_Auth::getAuth(), $perm, null, $this, $allLevels);
+    }
+
+    /**
+     * Get all children of this share.
+     *
+     * @param int $perm           Horde_Perms::* constant. If NULL will return
+     *                            all shares regardless of permissions.
+     * @param boolean $allLevels  Return all levels.
+     *
+     * @return mixed  An array of Horde_Share_Object objects || PEAR_Error
+     */
+    function getChildren($perm = Horde_Perms::SHOW, $allLevels = true)
+    {
+        return $this->_shareOb->listShares(Horde_Auth::getAuth(), $perm, null, 0, 0,
+             null, 1, $this, $allLevels, is_null($perm));
+
+    }
+
+    /**
+     * Returns a child's direct parent
+     *
+     * @return mixed  The direct parent Horde_Share_Object or PEAR_Error
+     */
+    function getParent()
+    {
+        return $this->_shareOb->getParent($this);
+    }
+
+    /**
+     * Get all of this share's parents.
+     *
+     * @return array()  An array of Horde_Share_Objects
+     */
+    function getParents()
+    {
+        $parents = array();
+        $share = $this->getParent();
+        while (is_a($share, 'Horde_Share_Object')) {
+            $parents[] = $share;
+            $share = $share->getParent();
+        }
+        return array_reverse($parents);
+    }
+
+    /**
+     * Set the parent object for this share.
+     *
+     * @param mixed $parent    A Horde_Share object or share id for the parent.
+     *
+     * @return mixed  true || PEAR_Error
+     */
+    function setParent($parent)
+    {
+        if (!is_null($parent) && !is_a($parent, 'Horde_Share_Object')) {
+            $parent = $this->_shareOb->getShareById($parent);
+            if (is_a($parent, 'PEAR_Error')) {
+                Horde::logMessage($parent, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $parent;
+            }
+        }
+
+        /* If we are an existing share, check for any children */
+        if ($this->getId()) {
+            $children = $this->_shareOb->listShares(
+                Horde_Auth::getAuth(), Horde_Perms::EDIT, null, 0, 0, null, 0,
+                $this->getId());
+        } else {
+            $children = array();
+        }
+
+        /* Can't set a child share as a parent */
+        if (!empty($parent) && in_array($parent->getId(), array_keys($children))) {
+            return PEAR::raiseError('Cannot set an existing child as the parent');
+        }
+
+        if (!is_null($parent)) {
+            $parent_string = $parent->get('parents') . ':' . $parent->getId();
+        } else {
+            $parent_string = null;
+        }
+        $this->data['share_parents'] = $parent_string;
+        $query = $this->_shareOb->_write_db->prepare('UPDATE ' . $this->_shareOb->_table . ' SET share_parents = ? WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
+        $result = $query->execute(array($this->data['share_parents'], $this->getId()));
+        $query->free();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Now we can reset the children's parent */
+        foreach($children as $child) {
+            $child->setParent($this);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the permission of this share.
+     *
+     * @return Horde_Perms_Permission  Permission object that represents the
+     *                               permissions on this share.
+     */
+    function &getPermission()
+    {
+        $perm = new Horde_Perms_Permission('');
+        $perm->data = isset($this->data['perm'])
+            ? $this->data['perm']
+            : array();
+
+        return $perm;
+    }
+
+    /**
+     * Returns one of the attributes of the object, or null if it isn't
+     * defined.
+     *
+     * @param string $attribute  The attribute to retrieve.
+     *
+     * @return mixed  The value of the attribute, or an empty string.
+     */
+    function _get($attribute)
+    {
+        if ($attribute == 'owner' || $attribute == 'parents') {
+            return $this->data['share_' . $attribute];
+        } elseif (isset($this->data['attribute_' . $attribute])) {
+            return $this->data['attribute_' . $attribute];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Hierarchical shares do not have share names.
+     *
+     * @return unknown
+     */
+    function _getName()
+    {
+        return '';
+    }
+
+}
diff --git a/framework/Share/package.xml b/framework/Share/package.xml
new file mode 100644 (file)
index 0000000..6c08176
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Share</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Shared Permissions System</summary>
+ <description>Horde_Share:: This class provides an interface to all shared
+resources a user owns or has access to.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>23:21:27</time>
+ <version>
+  <release>0.0.3</release>
+  <api>0.0.3</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Converted to package.xml 2.0 for pear.horde.org.
+* Split into a driver pattern.
+* Simplified the API greatly, removing unused functions.
+* Add a method for counting the number of shares at a certain access level.
+* Add native SQL driver (duck@obala.net, Request #6109).
+* Implemented extended free/busy access concept in the Kolab driver.
+ </notes>
+ <contents>
+  <dir name="/">
+   <file baseinstalldir="/Horde" name="Share.php" role="php"/>
+   <dir name="Share">
+     <file baseinstalldir="/Horde" name="datatree.php" role="php"/>
+     <file baseinstalldir="/Horde" name="kolab.php" role="php"/>
+     <file baseinstalldir="/Horde" name="sql.php" role="php"/>
+     <file baseinstalldir="/Horde" name="sql_hierarchical.php" role="php"/>
+   </dir>
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Perms</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_Prefs</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-10-29</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>- Added countShares().
+- listShares() and countShares() now treat the $owner argument
+  as an optional username restricting the returned shares, not
+  a modifier to the initial $userid. Now $userid is the user
+  who must have permission to see the shares that are returned
+  or counted, and $owner, if non-null, limits the shares
+  returned to those owned by $owner.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-05</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Share/tests/Horde/Share/AllTests.php b/framework/Share/tests/Horde/Share/AllTests.php
new file mode 100644 (file)
index 0000000..1d93893
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+/**
+ * All tests for the Horde_Share:: package.
+ *
+ * $Horde: framework/Share/tests/Horde/Share/AllTests.php,v 1.2 2009/01/06 17:49:46 jan Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Horde
+ * @package    Share
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Share
+ */
+
+/**
+ * Define the main method 
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Share_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * Combine the tests for this package.
+ *
+ * $Horde: framework/Share/tests/Horde/Share/AllTests.php,v 1.2 2009/01/06 17:49:46 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category   Horde
+ * @package    Share
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Share
+ */
+class Horde_Share_AllTests
+{
+
+    /**
+     * Main entry point for running the suite.
+     *
+     * @return NULL
+     */
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    /**
+     * Collect the unit tests of this directory into a new suite.
+     *
+     * @return PHPUnit_Framework_TestSuite The test suite.
+     */
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Share');
+
+        $basedir    = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/",
+                                                  '\\1', $pathname));
+                $suite->addTestSuite('Horde_Share_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Share_AllTests::main') {
+    Horde_Share_AllTests::main();
+}
diff --git a/framework/Share/tests/Horde/Share/KolabScenarioTest.php b/framework/Share/tests/Horde/Share/KolabScenarioTest.php
new file mode 100644 (file)
index 0000000..a2d0480
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Handling Kolab shares.
+ *
+ * $Horde: framework/Share/tests/Horde/Share/KolabScenarioTest.php,v 1.4 2009/03/20 23:47:03 wrobel Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Horde
+ * @package    Share
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Share
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/Storage.php';
+
+/**
+ * Handling groups.
+ *
+ * $Horde: framework/Share/tests/Horde/Share/KolabScenarioTest.php,v 1.4 2009/03/20 23:47:03 wrobel Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Server
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+class Horde_Share_KolabScenarioTest extends Horde_Kolab_Test_Storage
+{
+    /**
+     * Test listing shares.
+     *
+     * @scenario
+     *
+     * @return NULL
+     */
+    public function listingShares()
+    {
+        $this->given('a populated Kolab setup')
+            ->when('logging in as a user with a password', 'wrobel', 'none')
+            ->and('create a Kolab default calendar with name', "Calendar")
+            ->and('retrieving the list of shares for the application', 'kronolith')
+            ->then('the login was successful')
+            ->and('the creation of the folder was successful')
+            ->and('the list contains a share named', 'wrobel@example.org')
+            ->and('the list contains a number of elements equal to', 1);
+    }
+}
\ No newline at end of file
diff --git a/framework/Share/tests/kolab_createdefault.phpt b/framework/Share/tests/kolab_createdefault.phpt
new file mode 100644 (file)
index 0000000..4332ae6
--- /dev/null
@@ -0,0 +1,47 @@
+--TEST--
+Check the Kolab Share handler
+--FILE--
+<?php
+
+require_once 'Horde/Kolab/Test/Storage.php';
+$test = new Horde_Kolab_Test_Storage();
+
+$world = $test->prepareBasicSetup();
+
+$test->assertTrue($world['auth']->authenticate('wrobel@example.org',
+                                               array('password' => 'none')));
+
+$test->prepareNewFolder($world['storage'], 'Contacts', 'contact', true);
+
+require_once dirname(__FILE__) . '/../Share.php';
+                               
+$shares = Horde_Share::singleton('kronolith', 'kolab');
+
+class Perms_mock 
+{
+    function &newPermission($name) {
+        $perms = array(
+            'users' => array(
+                Horde_Auth::getAuth() => Horde_Perms::SHOW | Horde_Perms::READ |
+                Horde_Perms::EDIT | Horde_Perms::DELETE));
+        $result = &new Horde_Perms_Permission($name, $perms);
+        return $result;
+    }
+}
+
+$GLOBALS['perms'] = &new Perms_mock();
+
+$default = $shares->getDefaultShare();
+
+echo $default->getName() . "\n";
+
+$shares = Horde_Share::singleton('turba', 'kolab');
+
+$default = $shares->getDefaultShare();
+
+echo $default->get('name') . "\n";
+
+?>
+--EXPECT--
+wrobel@example.org
+Contacts
diff --git a/framework/Share/tests/kolab_list.phpt b/framework/Share/tests/kolab_list.phpt
new file mode 100644 (file)
index 0000000..6e5a84f
--- /dev/null
@@ -0,0 +1,26 @@
+--TEST--
+Check the Kolab Share handler
+--FILE--
+<?php
+
+require_once 'Horde/Kolab/Test/Storage.php';
+$test = new Horde_Kolab_Test_Storage();
+
+$world = $test->prepareBasicSetup();
+
+$test->assertTrue($world['auth']->authenticate('wrobel@example.org',
+                                               array('password' => 'none')));
+
+$test->prepareNewFolder($world['storage'], 'Calendar', 'event');
+
+require_once dirname(__FILE__) . '/../Share.php';
+
+$shares = Horde_Share::singleton('kronolith', 'kolab');
+
+$keys = array_keys($shares->listShares('wrobel@example.org'));
+foreach ($keys as $key) {
+  echo $key . "\n";
+}
+?>
+--EXPECT--
+INBOX%2FCalendar
diff --git a/framework/Share/tests/kolab_simple.phpt b/framework/Share/tests/kolab_simple.phpt
new file mode 100644 (file)
index 0000000..c701f50
--- /dev/null
@@ -0,0 +1,14 @@
+--TEST--
+Check the Kolab Share handler
+--FILE--
+<?php
+
+$conf['kolab']['enabled'] = true;
+
+require_once 'PEAR.php';
+
+require_once dirname(__FILE__) . '/../Share.php';
+
+$shares = Horde_Share::singleton('test', 'kolab');
+?>
+--EXPECT--
diff --git a/framework/SyncML/SyncML.php b/framework/SyncML/SyncML.php
new file mode 100644 (file)
index 0000000..48bd75a
--- /dev/null
@@ -0,0 +1,556 @@
+<?php
+/**
+ * There are two global objects that are used by SyncML:
+ * 1) $_SESSION['SyncML.state']:
+ *    session object used to maintain the state between the individual
+ *    SyncML messages.
+ *
+ * 2) $GLOBALS['backend']
+ *    Backend to handle the communication with the datastore.
+ *
+ * @todo: Main Todos:
+ * - ensure that no server data is written for ALERT_ONE_WAY_FROM_SERVER
+ *   even when client sends data (security!)
+ * - consinstant naming of clientSyncDB (currently called targetLocURI, db
+ *   or synctype)
+ * - tackle the AddReplace issue: when a Replace is issued (i.e. during
+ *   SlowSync) the server should first check if the entry already exists.
+ *   Like: does a calendar entry with the same timeframe, same subject and
+ *   location exist. If so, the replace should replace this value rather than
+ *   create a new one as a duplicate.
+ *
+ * $Horde: framework/SyncML/SyncML.php,v 1.79 2009/07/14 00:25:30 mrubinsk Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+
+require_once 'SyncML/State.php';
+require_once 'SyncML/Command.php';
+require_once 'SyncML/Command/SyncHdr.php';
+require_once 'SyncML/Sync.php';
+require_once 'SyncML/XMLOutput.php';
+require_once 'SyncML/Backend.php';
+
+/* PEAR_LOG_* constants */
+require_once 'Log.php';
+
+class SyncML_ContentHandler {
+
+    /**
+     * Stack for holding the xml elements during creation of the object from
+     * the xml event flow.
+     *
+     * @var array
+     */
+    var $_Stack = array();
+
+    /**
+     * @var string
+     */
+    var $_chars;
+
+    /**
+     * Instance of SyncML_Command. Events are passed through to this
+     * ContentHandler.
+     *
+     * @var SyncML_Command
+     */
+    var $_currentCommand;
+
+    /**
+     * Whether we received a final element in this message.
+     */
+    var $_gotFinal = false;
+
+    var $_xmlWriter;
+
+    var $_wbxmlparser = null;
+
+    /**
+     * The response URI as sent by the server.
+     *
+     * This is the endpoint URL of the RPC server.
+     *
+     * @var string
+     */
+    var $_respURI;
+
+    var $debug = false;
+
+    function SyncML_ContentHandler()
+    {
+        /* Set to true to indicate that we expect another message from the
+         * client. If this is still false at the end of processing, the sync
+         * session is finished and we can close the session. */
+        $GLOBALS['message_expectresponse'] = false;
+    }
+
+    /**
+     * Here's were all the processing takes place: gets the SyncML request
+     * data and returns a SyncML response. The only thing that needs to be in
+     * place before invoking this function is a working backend.
+     *
+     * @param string $request      The raw request string.
+     * @param string $contentType  The MIME content type of the request. Should
+     *                             be either application/vnd.syncml or
+     *                             application/vnd.syncml+wbxml.
+     * @param string $respURI      The url of the server endpoint. Will be
+     *                             returned in the RespURI element.
+     */
+    function process($request, $contentType, $respURI = null)
+    {
+        $isWBXML = $contentType =='application/vnd.syncml+wbxml';
+        $this->_respURI = $respURI;
+
+        /* Catch any errors/warnings/notices that may get thrown while
+         * processing. Don't want to let anything go to the client that's not
+         * part of the valid response. */
+        ob_start();
+
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_CLIENTMESSAGE, $request, $isWBXML);
+
+        if (!$isWBXML) {
+            /* XML code. */
+
+            /* try to extract charset from XML text */
+            if (preg_match('/^\s*<\?xml[^>]*encoding\s*=\s*"([^"]*)"/i',
+                           $request, $m)) {
+                $charset = $m[1];
+            } else {
+                $charset = 'UTF-8';
+            }
+
+            $GLOBALS['backend']->setCharset($charset);
+
+            /* Init output handler. */
+            $this->_xmlWriter = &SyncML_XMLOutput::singleton();
+            /* XML_WBXML_ContentHandler Is a class that produces plain XML
+             * output. */
+            require_once 'XML/WBXML/ContentHandler.php';
+            $this->_xmlWriter->init(new XML_WBXML_ContentHandler());
+
+            /* Create the XML parser and set method references. */
+            $parser = xml_parser_create_ns($charset);
+            xml_set_object($parser, $this);
+            xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
+            xml_set_element_handler($parser, '_startElement', '_endElement');
+            xml_set_character_data_handler($parser, '_characters');
+            xml_set_processing_instruction_handler($parser, '');
+            xml_set_external_entity_ref_handler($parser, '');
+
+            /* Here we go: fire off events: */
+            if (!xml_parse($parser, $request)) {
+                $s = sprintf('XML error: %s at line %d',
+                             xml_error_string(xml_get_error_code($parser)),
+                             xml_get_current_line_number($parser));
+                $GLOBALS['backend']->logMessage($s, __FILE__, __LINE__,
+                                                PEAR_LOG_ERR);
+                xml_parser_free($parser);
+                return new PEAR_Error($s);
+            }
+
+            xml_parser_free($parser);
+
+        } else {
+            /* WBXML code. */
+            require_once 'XML/WBXML/Decoder.php';
+            require_once 'XML/WBXML/Encoder.php';
+
+            /* The decoder works like the parser in the XML code above: It
+             * parses the input and calls the callback functions of $this. */
+            $this->_wbxmlparser = new XML_WBXML_Decoder();
+            $this->_wbxmlparser->setContentHandler($this);
+
+            /* Init output handler. */
+            $this->_xmlWriter = &SyncML_XMLOutput::singleton();
+            $this->_xmlWriter->init(new XML_WBXML_Encoder());
+
+            /* Here we go: fire off events: */
+            $r = $this->_wbxmlparser->decode($request);
+        }
+
+        $id = @session_id();
+        $sessionclose = empty($id);
+
+        $output = $this->getOutput();
+        if (!$isWBXML) {
+            $output = '<?xml version="1.0" encoding="' . $charset . '"?>' . $output;
+        }
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_SERVERMESSAGE, $output, $isWBXML, $sessionclose);
+
+        /* Clear the output buffer that we started above, and log anything
+         * that came up for later debugging. */
+        $errorLogging = ob_get_clean();
+
+        if (!empty($errorLogging)) {
+            $GLOBALS['backend']->logMessage('Caught output: ' . $errorLogging,
+                                            __FILE__, __LINE__,
+                                            PEAR_LOG_WARNING);
+        }
+
+        return $output;
+    }
+
+    /*
+     * CONTENTHANDLER CALLBACK FUNCTIONS
+     * The following functions are callback functions that are called by the
+     * XML parser. The XML and WBXML parsers use slightly different functions,
+     * so the methods are duplicated.
+     */
+
+    /**
+     * Returns the XML|WBXML output once processing is finished.
+     *
+     * @return string  The XML or WBXML output data.
+     */
+    function getOutput()
+    {
+        return $this->_xmlWriter->getOutput();
+    }
+
+    /**
+     * Callback function called by XML parser.
+     */
+    function _startElement($parser, $tag, $attributes)
+    {
+        list($uri, $name) = $this->_splitURI($tag);
+        $this->startElement($uri, $name, $attributes);
+    }
+
+    /**
+     * Callback function called by XML parser.
+     */
+    function _characters($parser, $chars)
+    {
+        $this->characters($chars);
+    }
+
+    /**
+     * Callback function called by XML parser.
+     */
+    function _endElement($parser, $tag)
+    {
+        list($uri, $name) = $this->_splitURI($tag);
+        $this->endElement($uri, $name);
+    }
+
+    /**
+     * Splits an URI as provided by the XML parser.
+     */
+    function _splitURI($tag)
+    {
+        $parts = explode(':', $tag);
+        $name = array_pop($parts);
+        $uri = implode(':', $parts);
+        return array($uri, $name);
+    }
+
+    /**
+     * Callback function called by WBXML parser.
+     */
+    function startElement($uri, $element, $attrs)
+    {
+        $this->_Stack[] = $element;
+
+        // <SyncML>: don't do anyhting yet
+        if (count($this->_Stack) == 1) {
+            return;
+        }
+
+        // header or body?
+        if ($this->_Stack[1] == 'SyncHdr') {
+            if (count($this->_Stack) == 2) {
+                $this->_currentCommand = new SyncML_Command_SyncHdr($this->_xmlWriter);
+            }
+            $this->_currentCommand->startElement($uri, $element, $attrs);
+        } else {
+            switch (count($this->_Stack)) {
+            case 2:
+                 // <SyncBody>: do nothing yet
+                 break;
+            case 3:
+                // new Command:
+                // <SyncML><SyncBody><[Command]>
+                $this->_currentCommand = &SyncML_Command::factory($element,$this->_xmlWriter);
+                $this->_currentCommand->startElement($uri, $element, $attrs);
+                break;
+            default:
+                // pass on to current command handler:
+                // <SyncML><SyncBody><Command><...>
+                $this->_currentCommand->startElement($uri, $element, $attrs);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Callback function called by WBXML parser.
+     */
+    function endElement($uri, $element)
+    {
+        // </SyncML>: everything done already by end of SyncBody
+        if (count($this->_Stack) == 1) {
+            return;
+        }
+        // header or body?
+        if ($this->_Stack[1] == 'SyncHdr') {
+            switch (count($this->_Stack)) {
+            case 2:
+                // </SyncHdr> end of header
+                $this->handleHeader($this->_currentCommand);
+                if ($this->debug) {
+                    var_dump($this->_currentCommand);
+                }
+                unset($this->_currentCommand);
+                break;
+            default:
+                // pass on to command handler:
+                $this->_currentCommand->endElement($uri, $element);
+                break;
+            }
+        } else {
+            switch (count($this->_Stack)) {
+            case 2:
+                // </SyncBody> end of SyncBody. Finish everything:
+                $this->handleEnd();
+                break;
+            case 3:
+                // </[Command]></SyncBody></SyncML>
+                // Command finished. Complete parsing and pass on to Handler
+                $this->_currentCommand->endElement($uri, $element);
+                $this->handleCommand($this->_currentCommand);
+                if ($this->debug) {
+                    var_dump($this->_currentCommand);
+                }
+                unset($this->_currentCommand);
+                break;
+            default:
+                // </...></[Command]></SyncBody></SyncML>
+                // pass on to command handler:
+                $this->_currentCommand->endElement($uri, $element);
+                break;
+            }
+        }
+
+        if (isset($this->_chars)) {
+            unset($this->_chars);
+        }
+
+        array_pop($this->_Stack);
+    }
+
+    /**
+     * Callback function called by WBXML parser.
+     */
+    function characters($str)
+    {
+        if (isset($this->_currentCommand)) {
+            $this->_currentCommand->characters($str);
+        } else {
+            if (isset($this->_chars)) {
+                $this->_chars = $this->_chars . $str;
+            } else {
+                $this->_chars = $str;
+            }
+        }
+    }
+
+    /*
+     * PROCESSING FUNCTIONS
+     *
+     * The following functions are called by the callback functions
+     * and do the actual processing.
+     */
+
+    /**
+     * Handles the header logic.
+     *
+     * Invoked after header is parsed.
+     */
+    function handleHeader(&$hdr)
+    {
+        if (is_object($this->_wbxmlparser)) {
+            /* The WBXML parser only knows about the charset once parsing is
+             * started. So setup charset now. */
+            $this->_xmlWriter->_output->setVersion($this->_wbxmlparser->getVersion());
+            $this->_xmlWriter->_output->setCharset($this->_wbxmlparser->getCharsetStr());
+            $GLOBALS['backend']->setCharset($this->_wbxmlparser->getCharsetStr());
+        }
+
+        /* Start the session. */
+        $hdr->setupState();
+        $state = &$_SESSION['SyncML.state'];
+        $state->wbxml = $this->_xmlWriter->isWBXML();
+
+        /* Check auth. */
+        if (!$state->authenticated) {
+            $auth = $GLOBALS['backend']->checkAuthentication(
+                $hdr->user, $hdr->credData, $hdr->credFormat, $hdr->credType);
+            if ($auth !== false) {
+                $state->authenticated = true;
+                $statuscode = RESPONSE_AUTHENTICATION_ACCEPTED;
+                $state->user = $auth;
+                $GLOBALS['backend']->setUser($auth);
+            } else {
+                if (!$hdr->credData) {
+                    $statuscode = RESPONSE_CREDENTIALS_MISSING;
+                } else {
+                    $statuscode = RESPONSE_INVALID_CREDENTIALS;
+                }
+                $GLOBALS['backend']->logMessage('Invalid authentication',
+                                                __FILE__, __LINE__,
+                                                PEAR_LOG_DEBUG);
+            }
+        } else {
+            $statuscode = RESPONSE_OK;
+            $GLOBALS['backend']->setUser($state->user);
+        }
+
+        /* Create <SyncML>. */
+        $this->_xmlWriter->outputInit();
+
+        /* Got the state; now write our SyncHdr header. */
+        $this->_xmlWriter->outputHeader($this->_respURI);
+
+        /* Creates <SyncBody>. */
+        $this->_xmlWriter->outputBodyStart();
+
+        /* Output status for SyncHdr. */
+        $this->_xmlWriter->outputStatus('0', 'SyncHdr', $statuscode,
+                                        $state->targetURI,
+                                        $state->sourceURI);
+
+        /* Debug logging string. */
+        $str = 'Authenticated: ' . ($state->authenticated ? 'yes' : 'no')
+            . '; version: ' . $state->getVerDTD()
+            . '; message ID: ' . $state->messageID
+            . '; source URI: ' . $state->sourceURI
+            . '; target URI: ' . $state->targetURI
+            . '; user: ' . $state->user
+            . '; charset: ' . $GLOBALS['backend']->getCharset()
+            . '; wbxml: ' . ($state->wbxml ? 'yes' : 'no');
+
+        $GLOBALS['backend']->logMessage($str, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+    }
+
+    /**
+     * Processes one command after it has been completely parsed.
+     *
+     * Invoked after a command is parsed.
+     */
+    function handleCommand(&$cmd)
+    {
+        $name = $cmd->getCommandName();
+        if ($name != 'Status' && $name != 'Map' && $name != 'Final' &&
+            $name != 'Sync' && $name != 'Results') {
+            /* We've got to do something! This can't be the last packet. */
+            $GLOBALS['message_expectresponse'] = true;
+        }
+        if ($name == 'Final') {
+            $this->_gotFinal = true;
+        }
+        /* Actual processing takes place here. */
+        $cmd->handleCommand($this->debug);
+    }
+
+    /**
+     * Finishes the response.
+     *
+     * Invoked after complete message is parsed.
+     */
+    function handleEnd()
+    {
+        global $messageFull;
+
+        $state = &$_SESSION['SyncML.state'];
+
+        /* If there's pending sync data and space left in the message, send
+         * data now. */
+        if ($messageFull || $state->hasPendingSyncs()) {
+            /* still something to do: don't close session. */
+            $GLOBALS['message_expectresponse'] = true;
+        }
+
+        if (!$messageFull &&
+            count($p = $state->getPendingSyncs()) > 0) {
+            foreach ($p as $pendingSync) {
+                if (!$messageFull) {
+                   $GLOBALS['backend']->logMessage(
+                       'Continuing sync for syncType ' . $pendingSync,
+                       __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    $sync = &$state->getSync($pendingSync);
+                    $sync->createSyncOutput($this->_xmlWriter);
+                }
+            }
+        }
+
+        if (isset($state->curSyncItem)) {
+            $this->_xmlWriter->outputAlert(
+                ALERT_NO_END_OF_DATA,
+                $state->curSyncItem->sync->getClientLocURI(),
+                $state->curSyncItem->sync->getServerLocURI(),
+                $state->curSyncItem->sync->getServerAnchorLast(),
+                $state->curSyncItem->sync->getServerAnchorNext());
+        }
+
+        /* Don't send the final tag if we haven't sent all sync data yet. */
+        if ($this->_gotFinal) {
+            if (!$messageFull &&
+                !$state->hasPendingSyncs()) {
+                /* Create <Final></Final>. */
+                $this->_xmlWriter->outputFinal();
+                $GLOBALS['backend']->logMessage('Sending <Final> to client',
+                                                __FILE__, __LINE__,
+                                                PEAR_LOG_DEBUG);
+                $state->delayedFinal = false;
+            } else {
+                $GLOBALS['message_expectresponse'] = true;
+                /* Remember to send a Final. */
+                $state->delayedFinal = true;
+            }
+        } elseif ($state->delayedFinal) {
+            if (!$messageFull &&
+                !$state->hasPendingSyncs()) {
+                /* Create <Final></Final>. */
+                $this->_xmlWriter->outputFinal();
+                $GLOBALS['backend']->logMessage(
+                    'Sending delayed <Final> to client',
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $state->delayedFinal = false;
+            } else {
+                $GLOBALS['message_expectresponse'] = true;
+            }
+        }
+
+        /* Create </SyncML>. Message is finished now! */
+        $this->_xmlWriter->outputEnd();
+
+        if ($this->_gotFinal &&
+            !$GLOBALS['message_expectresponse'] &&
+            $state->isAllSyncsComplete()) {
+            /* This packet did not contain any real actions, just status and
+             * map. This means we're done. The session can be closed and the
+             * anchors saved for the next sync. */
+            foreach ($state->getSyncs() as $sync) {
+                $sync->closeSync();
+            }
+            $GLOBALS['backend']->logMessage('Session completed and closed',
+                                            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            /* Session can be closed here. */
+            $GLOBALS['backend']->sessionClose();
+        } else {
+            $GLOBALS['backend']->logMessage('Return message completed',
+                                            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Backend.php b/framework/SyncML/SyncML/Backend.php
new file mode 100644 (file)
index 0000000..998dfca
--- /dev/null
@@ -0,0 +1,1047 @@
+<?php
+/**
+ * A SyncML Backend provides the interface between the SyncML protocol as
+ * provided by the SyncML pear package and an actual calendar or address book
+ * application. This "actual application" is called the "data store" in this
+ * description.
+ *
+ * The backend provides the following groups of functions:
+ *
+ * 1) Access to the datastore
+ *    Reading, adding, replacing and deleting of entries.  Also retrieve
+ *    information about changes in data store.  This is done via the
+ *    retrieveEntry(), addEntry(), replaceEntry(), deleteEntry() and
+ *    getServerChanges() methods.
+ *
+ * 2) User management functions
+ *    This is the checkAuthentication() method to verify that a given user
+ *    password combination is allowed to access the backend data store, and
+ *    the setUser() method which does a "login" to the backend data store if
+ *    required by the type of backend data store. Please note that the
+ *    password is only transferred once in a sync session, so when handling
+ *    the subsequent packets messages, the user may need to be "logged in"
+ *    without a password. (Or the session management keeps the user "logged
+ *    in").
+ *
+ * 3) Maintainig the client ID <-> server ID map
+ *    The SyncML protocol does not require clients and servers to use the same
+ *    primary keys for the data entries. So a map has to be in place to
+ *    convert between client primary keys (called cuid's here) and server
+ *    primary keys (called suid's). It's up to the server to maintain this
+ *    map.  Method for this is createUidMap().
+ *
+ * 4) Sync anchor handling
+ *    After a successful initial sync, the client and server sync timestamps
+ *    are stored. This allows to perform subsequent syncs as delta syncs,
+ *    where only new changes are replicated. Servers as well as clients need
+ *    to be able to store two sync anchors (the client's and the server's) for
+ *    a sync. Methods for this are readSyncAnchors() and writeSyncAnchors().
+ *
+ * 5) Test supporting functions
+ *    The SyncML module comes with its own testing framework. All you need to
+ *    do is implement the two methods testSetup() and testTearDown() and you
+ *    are able to test your backend with all the test cases that are part of
+ *    the module.
+ *
+ * 6) Miscellaneous functions
+ *    This involves session handling (sessionStart() and sessionClose()),
+ *    logging (logMessage() and logFile()), timestamp creation
+ *    (getCurrentTimeStamp()), charset handling (getCharset(), setCharset())
+ *    and database identification (isValidDatabaseURI()). For all of these
+ *    functions, a default implementation is provided in SyncML_Backend.
+ *
+ * If you want to create a backend for your own appliction, you can either
+ * derive from SyncML_Backend and implement everything in groups 1 to 5 or you
+ * derive from SyncML_Backend_Sql which implements an example backend based on
+ * direct database access using the PEAR MDB2 package. In this case you only
+ * need to implement groups 1 to 3 and can use the implementation from
+ * SyncML_Backend_Sql as a guideline for these functions.
+ *
+ * Key Concepts
+ * ------------
+ * In order to successfully create a backend, some understanding of a few key
+ * concepts in SyncML and the SyncML package are certainly helpful.  So here's
+ * some stuff that should make some issues clear (or at lest less obfuscated):
+ *
+ * 1) DatabaseURIs and Databases
+ *    The SyncML protocol itself is completly independant from the data that
+ *    is replicated. Normally the data are calendar or address book entries
+ *    but it may really be anything from browser bookmarks to comeplete
+ *    database tables. An ID (string name) of the database you want to
+ *    actually replicate has to be configured in the client. Typically that's
+ *    something like 'calendar' or 'tasks'. Client and server must agree on
+ *    these names.  In addition this string may be used to provide additional
+ *    arguments.  These are provided in a HTTP GET query style: like
+ *    tasks?ignorecompletedtasks to replicate only pending tasks. Such a "sync
+ *    identifier" is called a DatabaseURI and is really a database name plus
+ *    some additional options.
+ *    The SyncML package completly ignores these options and simply passes
+ *    them on to the backend. It's up to the backend to decide what to do with
+ *    them. However when dealing with the internal maps (cuid<->suid and sync
+ *    anchors), it's most likely to use the database name only rather than the
+ *    full databaseURI. The map information saying that server entry
+ *    20070101203040xxa@mypc.org has id 768 in the client device is valid for
+ *    the database "tasks", not for "tasks?somesillyoptions". So what you
+ *    normally do is calling some kind of <code>$database =
+ *    $this->_normalize($databaseURI)</cod> in every backend method that deals
+ *    with databaseURIs and use $database afterwards. However actual usage of
+ *    options is up to the backend implementation. SyncML works fine without.
+ *
+ * 2) Suid and Guid mapping
+ *    This is the mapping of client IDs to server IDs and vice versa.  Please
+ *    note that this map is per user and per client device: the server entry
+ *    20070101203040xxa@mypc.org may have ID 720 in your PDA and AA10FC3A in
+ *    your mobile phone.
+ *
+ * 3) Sync Anchors
+ *    @todo describe sync anchors
+ *    Have a look at the SyncML spec
+ *    http://www.openmobilealliance.org/tech/affiliates/syncml/syncmlindex.html
+ *    to find out more.
+ *
+ * 4) Changes and Timestamps
+ *    @todo description of Changes and Timestamps, "mirroring effect"
+ *    This is real tricky stuff.
+ *    First it's important to know, that the SyncML protocol requires the
+ *    ending timestamp of the sync timeframe to be exchanged _before_ the
+ *    actual syncing starts. So all changes made during a sync have timestamps
+ *    that are in the timeframe for the next upcoming sync.  Data exchange in
+ *    a sync session works in two steps: 1st) the clients sends its changes to
+ *    the server, 2nd) the server sends its changes to the client.
+ *    So when in step 2, the backend datastore API is called with a request
+ *    like "give me all changes in the server since the last sync".  Thus you
+ *    also get the changes induced by the client in step 1 as well.  You have
+ *    to somehow "tag" them to avoid echoing (and thus duplicating) them back
+ *    to the client. Simply storing the guids in the session is not
+ *    sufficient: the changes are made _after_ the end timestamp (see 1) of
+ *    the current sync so you'll dupe them in the next sync.
+ *    The current implementation deals with this as follows: whenever a client
+ *    induced change is done in the backend, the timestamp for this change is
+ *    stored in the cuid<->suid map in an additional field. That's the perfect
+ *    place as the tagging needs to be done "per client device": when an add
+ *    is received from the PDA it must not be sent back as an add to this
+ *    device, but to mobile phone it must be sent.
+ *    This is sorted out during the getServerChanges() process: if a server
+ *    change has a timestamp that's the same as in the guid<->suid map, it
+ *    came from the client and must not be added to the list of changes to be
+ *    sent to this client.
+ *    See the description of SyncML_Backend_Sql::_getChangeTS() for some more
+ *    information.
+ *
+ * 5) Messages and Packages
+ *    A message is a single HTTP Request. A package is single "logical
+ *    message", a sync step. Normally the two coincide. However due to message
+ *    size restrictions one package may be transferred in multiple messages
+ *    (HTTP requests).
+ *
+ * 7) Server mode, client mode and test mode
+ *    Per default, a backend is used for an SyncML server. Regarding the
+ *    SyncML protocol, the working of client and server is similar, except
+ *    that
+ *    a) the client initiates the sync requests and the server respons to them,
+ *       and
+ *    b) the server must maintain the client id<->server id map.
+ *
+ *    Currently the SyncML package is designed to create servers. But is's an
+ *    obvious (and straightforward) extension to do it for clients as well.
+ *    And as a client has actually less work to do than a server, the backend
+ *    should work for servers _and_ clients. During the sessionStart(), the
+ *    backend gets a parameter to let it know whether it's in client or server
+ *    mode (or test, see below). When in client mode, it should behave
+ *    slightly different:
+ *    a) the client doesn't do suid<->cuid mapping, so all invokations to the
+ *       map creation method createUidMap().
+ *    b) the client has only client ids, no server ids. So all arguments are
+ *       considered cuids even when named suid. See the SyncML_Backend_Sql
+ *       implementation, it's actually not that difficult.
+ *
+ *    Finally there's the test mode. The test cases consist of replaying
+ *    pre-recorded sessions. For that to work, the test script must "simulate"
+ *    user entries in the server data store. To do so, it creates a backend in
+ *    test mode. This behaves similar to a client: when an server entry is
+ *    created (modified) using addEntry() (replaceEntry()), no map entry must
+ *    be done.
+ *    The test backend uses also the two methods testSetup() and
+ *    testTearDown() to create a clean (empty) enviroment for the test user
+ *    "syncmltest".  See the SyncML_Backend_Sql implementation for details.
+ *
+ * $Horde: framework/SyncML/SyncML/Backend.php,v 1.60 2009/09/15 13:41:32 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+
+/** Types of logfiles. See logFile() method. */
+define ('SYNCML_LOGFILE_CLIENTMESSAGE', 1);
+define ('SYNCML_LOGFILE_SERVERMESSAGE', 2);
+define ('SYNCML_LOGFILE_DEVINF',        3);
+define ('SYNCML_LOGFILE_DATA',          4);
+
+/** Backend modes. */
+define ('SYNCML_BACKENDMODE_SERVER', 1);
+define ('SYNCML_BACKENDMODE_CLIENT', 2);
+define ('SYNCML_BACKENDMODE_TEST',   3);
+
+class SyncML_Backend {
+
+    /**
+     * The concatenated log messages.
+     *
+     * @var string
+     */
+    var $_logtext = '';
+
+    /**
+     * The directory where debugging information is stored.
+     *
+     * @see SyncML_Backend()
+     * @var string
+     */
+    var $_debugDir;
+
+    /**
+     * Whether to save SyncML messages in the debug directory.
+     *
+     * @see SyncML_Backend()
+     * @var boolean
+     */
+    var $_debugFiles;
+
+    /**
+     * The log level. One of the PEAR_LOG_* constants.
+     *
+     * @see PEAR Log package
+     * @see SyncML_Backend()
+     * @var integer
+     */
+    var $_logLevel = PEAR_LOG_INFO;
+
+    /**
+     * The charset used in the SyncML messages.
+     *
+     * @var string
+     */
+    var $_charset;
+
+    /**
+     * The current user.
+     *
+     * @var string
+     */
+    var $_user;
+
+    /**
+     * The ID of the client device.
+     *
+     * This is used for all data access as an ID to allow to distinguish
+     * between syncs with different devices.  $this->_user together with
+     * $this->_syncDeviceID is used as an additional key for all persistence
+     * operations.
+     *
+     * @var string
+     */
+    var $_syncDeviceID;
+
+    /**
+     * The backend mode. One of the SYNCML_BACKENDMODE_* constants.
+     *
+     * @var integer
+     */
+    var $_backendMode;
+
+    /**
+     * Constructor.
+     *
+     * Sets up the default logging mechanism.
+     *
+     * @param array $params  A hash with parameters. The following are
+     *                       supported by the default implementation.
+     *                       Individual backends may support other parameters.
+     *                       - debug_dir:   A directory to write debug output
+     *                                      to. Must be writeable by the web
+     *                                      server.
+     *                       - debug_files: If true, log all incoming and
+     *                                      outgoing packets and data
+     *                                      conversions and devinf log in
+     *                                      debug_dir.
+     *                       - log_level:   PEAR_LOG_*. Only log entries with
+     *                                      at least this level. Defaults to
+     *                                      PEAR_LOG_INFO.
+     */
+    function SyncML_Backend($params)
+    {
+        if (!empty($params['debug_dir']) && is_dir($params['debug_dir'])) {
+            $this->_debugDir = $params['debug_dir'];
+        }
+        $this->_debugFiles = !empty($params['debug_files']);
+        if (isset($params['log_level'])) {
+            $this->_logLevel = $params['log_level'];
+        }
+
+        $this->logMessage('Backend of class ' . get_class($this) . ' created',
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+     }
+
+    /**
+     * Attempts to return a concrete SyncML_Backend instance based on $driver.
+     *
+     * @param string $driver The type of concrete Backend subclass to return.
+     *                       The code is dynamically included from
+     *                       Backend/$driver.php if no path is given or
+     *                       directly with "include_once $driver . '.php'"
+     *                       if a path is included. So make sure this parameter
+     *                       is "safe" and not directly taken from web input.
+     *                       The class in the file must be named
+     *                       'SyncML_Backend_' . basename($driver) and extend
+     *                       SyncML_Backend.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return SyncML_Backend  The newly created concrete SyncML_Backend
+     *                         instance, or false on an error.
+     */
+    function factory($driver, $params = null)
+    {
+        if (empty($driver) || ($driver == 'none')) {
+            return false;
+        }
+
+        if (basename($driver) == $driver) {
+            include_once 'SyncML/Backend/' . $driver . '.php';
+        } else {
+            include_once $driver . '.php';
+        }
+
+        $driver = basename($driver);
+        $class = 'SyncML_Backend_' . $driver;
+        if (class_exists($class)) {
+            $backend = new $class($params);
+        } else {
+            return false;
+        }
+
+        return $backend;
+    }
+
+    /**
+     * Sets the charset.
+     *
+     * All data passed to the backend uses this charset and data returned from
+     * the backend must use this charset, too.
+     *
+     * @param string $charset  A valid charset.
+     */
+    function setCharset($charset)
+    {
+        $this->_charset = $charset;
+    }
+
+    /**
+     * Returns the charset.
+     *
+     * @return string  The charset used when talking to the backend.
+     */
+    function getCharset()
+    {
+        return $this->_charset;
+    }
+
+    /**
+     * Returns the current device's ID.
+     *
+     * @return string  The device ID.
+     */
+    function getSyncDeviceID()
+    {
+        return $this->_syncDeviceID;
+    }
+
+    /**
+     * Sets the user used for this session.
+     *
+     * This method is called by SyncML right after sessionStart() when either
+     * authentication is accepted via checkAuthentication() or a valid user
+     * has been retrieved from the state.  $this->_user together with
+     * $this->_syncDeviceID is used as an additional key for all persistence
+     * operations.
+     * This method may have to force a "login", when the backend doesn't keep
+     * auth state within a session or when in test mode.
+     *
+     * @param string $user  A user name.
+     */
+    function setUser($user)
+    {
+        $this->_user = $user;
+    }
+
+    /**
+     * Returns the current user.
+     *
+     * @return string  The current user.
+     */
+    function getUser()
+    {
+        return $this->_user;
+    }
+
+    /**
+     * Is called after the SyncML_State object has been set up, either
+     * restored from the session, or freshly created.
+     *
+     * @param SyncML_State  The current state object.
+     */
+    function setupState(&$state)
+    {
+    }
+
+    /**
+     * Starts a PHP session.
+     *
+     * @param string $syncDeviceID  The device ID.
+     * @param string $session_id    The session ID to use.
+     * @param integer $backendMode  The backend mode, one of the
+     *                              SYNCML_BACKENDMODE_* constants.
+     */
+    function sessionStart($syncDeviceID, $sessionId,
+                          $backendMode = SYNCML_BACKENDMODE_SERVER)
+    {
+        $this->_syncDeviceID = $syncDeviceID;
+        $this->_backendMode = $backendMode;
+
+        // Only the server needs to start a session:
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $sid = md5($syncDeviceID . $sessionId);
+            session_id($sid);
+            @session_start();
+        }
+    }
+
+    /**
+     * Closes the PHP session.
+     */
+    function sessionClose()
+    {
+        // Only the server needs to start a session:
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            session_unset();
+            session_destroy();
+        }
+    }
+
+    /**
+     * Returns whether a database URI is valid to be synced with this backend.
+     *
+     * This default implementation accepts "tasks", "calendar", "notes" and
+     * "contacts".  However individual backends may offer replication of
+     * different or completly other databases (like browser bookmarks or
+     * cooking recipes).
+     *
+     * @param string $databaseURI  URI of a database. Like calendar, tasks,
+     *                             contacts or notes. May include optional
+     *                             parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return boolean  True if a valid URI.
+     */
+    function isValidDatabaseURI($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        switch($database) {
+        case 'tasks';
+        case 'calendar';
+        case 'notes';
+        case 'contacts';
+            return true;
+
+        default:
+            $this->logMessage('Invalid database ' . $database
+                              . '. Try tasks, calendar, notes or contacts.',
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+    }
+
+    /**
+     * Returns entries that have been modified in the server database.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param integer $from_ts     Start timestamp.
+     * @param integer $to_ts       Exclusive end timestamp. Not yet
+     *                             implemented.
+     * @param array &$adds         Output array: hash of adds suid => 0
+     * @param array &$mods         Output array: hash of modifications
+     *                             suid => cuid
+     * @param array &$dels         Output array: hash of deletions suid => cuid
+     *
+     * @return mixed  True on success or a PEAR_Error object.
+     */
+    function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
+                              &$dels)
+    {
+        die('getServerChanges() not implemented!');
+    }
+
+    /**
+     * Retrieves an entry from the backend.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         Server unique id of the entry: for horde
+     *                             this is the guid.
+     * @param string $contentType  Content-Type: the MIME type in which the
+     *                             function should return the data.
+     *
+     * @return mixed  A string with the data entry or a PEAR_Error object.
+     */
+    function retrieveEntry($databaseURI, $suid, $contentType)
+    {
+        die('retrieveEntry() not implemented!');
+    }
+
+    /**
+     * Adds an entry into the server database.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return array  PEAR_Error or suid (Horde guid) of new entry
+     */
+    function addEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        die('addEntry() not implemented!');
+    }
+
+    /**
+     * Replaces an entry in the server database.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
+     */
+    function replaceEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        die('replaceEntry() not implemented!');
+    }
+
+    /**
+     * Deletes an entry from the server database.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     *
+     * @return boolean  True on success or false on failed (item not found).
+     */
+    function deleteEntry($databaseURI, $cuid)
+    {
+        die('deleteEntry() not implemented!');
+    }
+
+    /**
+     * Authenticates the user at the backend.
+     *
+     * For some types of authentications (notably auth:basic) the username
+     * gets extracted from the authentication data and is then stored in
+     * username.  For security reasons the caller must ensure that this is the
+     * username that is used for the session, overriding any username
+     * specified in <LocName>.
+     *
+     * @param string $username    Username as provided in the <SyncHdr>.
+     *                            May be overwritten by $credData.
+     * @param string $credData    Authentication data provided by <Cred><Data>
+     *                            in the <SyncHdr>.
+     * @param string $credFormat  Format of data as <Cread><Meta><Format> in
+     *                            the <SyncHdr>. Typically 'b64'.
+     * @param string $credType    Auth type as provided by <Cred><Meta><Type>
+     *                            in the <SyncHdr>. Typically
+     *                            'syncml:auth-basic'.
+     *
+     * @return boolean|string  The user name if authentication succeeded, false
+     *                         otherwise.
+     */
+    function checkAuthentication(&$username, $credData, $credFormat, $credType)
+    {
+        if (empty($credData) || empty($credType)) {
+            return false;
+        }
+
+        switch ($credType) {
+        case 'syncml:auth-basic':
+            list($username, $pwd) = explode(':', base64_decode($credData), 2);
+            $this->logMessage('Checking authentication for user ' . $username,
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return $this->_checkAuthentication($username, $pwd);
+
+        case 'syncml:auth-md5':
+            /* syncml:auth-md5 only transfers hash values of passwords.
+             * Currently the syncml:auth-md5 hash scheme is not supported
+             * by the authentication backend. So we can't use Horde to do
+             * authentication. Instead here is a very crude direct manual hook:
+             * To allow authentication for a user 'dummy' with password 'sync',
+             * run
+             * php -r 'print base64_encode(pack("H*",md5("dummy:sync")));'
+             * from the command line. Then create an entry like
+             *  'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw==' in the users array below,
+             * where the value is the command line output.
+             * This user/password combination is then accepted for md5-auth.
+             */
+            $users = array(
+                  // example for user dummy with pass pass:
+                  // 'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw=='
+                          );
+            if (empty($users[$username])) {
+                return false;
+            }
+
+            // @todo: nonce may be specified by client. Use it then.
+            $nonce = '';
+            if (base64_encode(pack('H*', md5($users[$username] . ':' . $nonce))) === $credData) {
+                return $this->_setAuthenticated($username, $credData);
+            }
+            return false;
+
+        default:
+            $this->logMessage('Unsupported authentication type ' . $credType,
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+    }
+
+    /**
+     * Authenticates the user at the backend.
+     *
+     * @abstract
+     *
+     * @param string $username    A user name.
+     * @param string $password    A password.
+     *
+     * @return boolean|string  The user name if authentication succeeded, false
+     *                         otherwise.
+     */
+    function _checkAuthentication($username, $password)
+    {
+        die('_checkAuthentication() not implemented!');
+    }
+
+    /**
+     * Sets a user as being authenticated at the backend.
+     *
+     * @abstract
+     *
+     * @param string $username    A user name.
+     * @param string $credData    Authentication data provided by <Cred><Data>
+     *                            in the <SyncHdr>.
+     *
+     * @return string  The user name.
+     */
+    function setAuthenticated($username, $credData)
+    {
+        die('setAuthenticated() not implemented!');
+    }
+
+    /**
+     * Stores Sync anchors after a successful synchronization to allow two-way
+     * synchronization next time.
+     *
+     * The backend has to store the parameters in its persistence engine
+     * where user, syncDeviceID and database are the keys while client and
+     * server anchor ar the payload. See readSyncAnchors() for retrieval.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI       URI of database to sync. Like calendar,
+     *                                  tasks, contacts or notes. May include
+     *                                  optional parameters:
+     *                                  tasks?options=ignorecompleted.
+     * @param string $clientAnchorNext  The client anchor as sent by the
+     *                                  client.
+     * @param string $serverAnchorNext  The anchor as used internally by the
+     *                                  server.
+     */
+    function writeSyncAnchors($databaseURI, $clientAnchorNext,
+                              $serverAnchorNext)
+    {
+    }
+
+    /**
+     * Reads the previously written sync anchors from the database.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return mixed  Two-element array with client anchor and server anchor as
+     *                stored in previous writeSyncAnchor() calls. False if no
+     *                data found.
+     */
+    function readSyncAnchors($databaseURI)
+    {
+    }
+
+    /**
+     * Creates a map entry to map between server and client IDs.
+     *
+     * If an entry already exists, it is overwritten.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     * @param string $suid         Server ID of the entry.
+     * @param integer $timestamp   Optional timestamp. This can be used to
+     *                             'tag' changes made in the backend during the
+     *                             sync process. This allows to identify these,
+     *                             and ensure that these changes are not
+     *                             replicated back to the client (and thus
+     *                             duplicated). See key concept "Changes and
+     *                             timestamps".
+     */
+    function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
+    {
+    }
+
+    /**
+     * Erases all mapping entries for one combination of user, device ID.
+     *
+     * This is used during SlowSync so that we really sync everything properly
+     * and no old mapping entries remain.
+     *
+     * @abstract
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     */
+    function eraseMap($databaseURI)
+    {
+    }
+
+    /**
+     * Logs a message in the backend.
+     *
+     * @param mixed $message     Either a string or a PEAR_Error object.
+     * @param string $file       What file was the log function called from
+     *                           (e.g. __FILE__)?
+     * @param integer $line      What line was the log function called from
+     *                           (e.g. __LINE__)?
+     * @param integer $priority  The priority of the message. One of:
+     *                           - PEAR_LOG_EMERG
+     *                           - PEAR_LOG_ALERT
+     *                           - PEAR_LOG_CRIT
+     *                           - PEAR_LOG_ERR
+     *                           - PEAR_LOG_WARNING
+     *                           - PEAR_LOG_NOTICE
+     *                           - PEAR_LOG_INFO
+     *                           - PEAR_LOG_DEBUG
+     */
+    function logMessage($message, $file, $line, $priority = PEAR_LOG_INFO)
+    {
+        if ($priority > $this->_logLevel)  {
+            return;
+        }
+
+        // Internal logging to logtext
+        if (is_string($this->_logtext)) {
+            switch ($priority) {
+            case PEAR_LOG_EMERG:
+                $this->_logtext .= 'EMERG:  ';
+                break;
+            case PEAR_LOG_ALERT:
+                $this->_logtext .= 'ALERT:  ';
+                break;
+            case PEAR_LOG_CRIT:
+                $this->_logtext .= 'CIRT:   ';
+                break;
+            case PEAR_LOG_ERR:
+                $this->_logtext .= 'ERR:    ';
+                break;
+            case PEAR_LOG_WARNING:
+                $this->_logtext .= 'WARNING:';
+                break;
+            case PEAR_LOG_NOTICE:
+                $this->_logtext .= 'NOTICE: ';
+                break;
+            case PEAR_LOG_INFO:
+                $this->_logtext .= 'INFO:   ';
+                break;
+            case PEAR_LOG_DEBUG:
+                $this->_logtext .= 'DEBUG:  ';
+                break;
+            default:
+                $this->_logtext .= 'UNKNOWN:';
+            }
+            if (is_string($message)) {
+                $this->_logtext .= $message;
+            } elseif (is_a($message, 'PEAR_Error')) {
+                $this->_logtext .= $message->getMessage();
+            }
+            $this->_logtext .= "\n";
+        }
+    }
+
+    /**
+     * Logs data to a file in the debug directory.
+     *
+     * @param integer $type          The data type. One of the SYNCML_LOGFILE_*
+     *                               constants.
+     * @param string $content        The data content.
+     * @param boolean $wbxml         Whether the data is wbxml encoded.
+     * @param boolean $sessionClose  Whether this is the last SyncML message
+     *                               in a session. Bump the file number.
+     */
+    function logFile($type, $content, $wbxml = false, $sessionClose = false)
+    {
+        if (empty($this->_debugDir) || !$this->_debugFiles) {
+            return;
+        }
+
+        switch ($type) {
+        case SYNCML_LOGFILE_CLIENTMESSAGE:
+            $filename = 'client_';
+            $mode = 'wb';
+            break;
+        case SYNCML_LOGFILE_SERVERMESSAGE:
+            $filename = 'server_';
+            $mode = 'wb';
+            break;
+        case SYNCML_LOGFILE_DEVINF:
+            $filename = 'devinf.txt';
+            $mode = 'wb';
+            break;
+        case SYNCML_LOGFILE_DATA:
+            $filename = 'data.txt';
+            $mode = 'a';
+            break;
+        default:
+            // Unkown type. Use $type as filename:
+            $filename = $type;
+            $mode = 'a';
+            break;
+        }
+
+        if ($type === SYNCML_LOGFILE_CLIENTMESSAGE ||
+            $type === SYNCML_LOGFILE_SERVERMESSAGE) {
+            $packetNum = @intval(file_get_contents($this->_debugDir
+                                                   . '/packetnum.txt'));
+            if (empty($packetNum)) {
+                $packetNum = 10;
+            }
+            if ($wbxml) {
+                $filename .= $packetNum . '.wbxml';
+            } else {
+                $filename .= $packetNum . '.xml';
+            }
+        }
+
+        /* Write file */
+        $fp = @fopen($this->_debugDir . '/' . $filename, $mode);
+        if ($fp) {
+            @fwrite($fp, $content);
+            @fclose($fp);
+        }
+
+        if ($type === SYNCML_LOGFILE_CLIENTMESSAGE) {
+            $this->logMessage('Started at ' . date('Y-m-d H:i:s')
+                              . '. Packet logged in '
+                              . $this->_debugDir . '/' . $filename,
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+
+        /* Increase packet number. */
+        if ($type === SYNCML_LOGFILE_SERVERMESSAGE) {
+            $this->logMessage('Finished at ' . date('Y-m-d H:i:s')
+                              . '. Packet logged in '
+                              . $this->_debugDir . '/' . $filename,
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $fp = @fopen($this->_debugDir . '/packetnum.txt', 'w');
+            if ($fp) {
+                /* When one complete session is finished: go to next 10th. */
+                if ($sessionClose) {
+                    $packetNum += 10 - $packetNum % 10;
+                } else {
+                    $packetNum += 1;
+                }
+                fwrite($fp, $packetNum);
+                fclose($fp);
+            }
+        }
+    }
+
+    /**
+     * Cleanup function called after all message processing is finished.
+     *
+     * Allows for things like closing databases or flushing logs.  When
+     * running in test mode, tearDown() must be called rather than close.
+     */
+    function close()
+    {
+        if (!empty($this->_debugDir)) {
+            $f = @fopen($this->_debugDir . '/log.txt', 'a');
+            if ($f) {
+                fwrite($f, $this->_logtext . "\n");
+                fclose($f);
+            }
+        }
+        session_write_close();
+    }
+
+    /**
+     * Returns the current timestamp in the same format as used by
+     * getServerChanges().
+     *
+     * Backends can use their own way to represent timestamps, like unix epoch
+     * integers or UTC Datetime strings.
+     *
+     * @return mixed  A timestamp of the current time.
+     */
+    function getCurrentTimeStamp()
+    {
+        /* Use unix epoch as default method for timestamps. */
+        return time();
+    }
+
+    /**
+     * Creates a clean test environment in the backend.
+     *
+     * Ensures there's a user with the given credentials and an empty data
+     * store.
+     *
+     * @abstract
+     *
+     * @param string $user This user accout has to be created in the backend.
+     * @param string $pwd  The password for user $user.
+     */
+    function testSetup($user, $pwd)
+    {
+        die('testSetup() not implemented!');
+    }
+
+    /**
+     * Prepares the test start.
+     *
+     * @param string $user This user accout has to be created in the backend.
+     */
+    function testStart($user)
+    {
+        die('testStart() not implemented!');
+    }
+
+    /**
+     * Tears down the test environment after the test is run.
+     *
+     * @abstract
+     *
+     * Should remove the testuser created during testSetup and all its data.
+     */
+    function testTearDown()
+    {
+        die('testTearDown() not implemented!');
+    }
+
+    /**
+     * Normalizes a databaseURI to a database name, so that
+     * _normalize('tasks?ignorecompleted') should return just 'tasks'.
+     *
+     * @param string $databaseURI  URI of a database. Like calendar, tasks,
+     *                             contacts or notes. May include optional
+     *                             parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return string  The normalized database name.
+     */
+    function _normalize($databaseURI)
+    {
+        $database = Horde_String::lower(
+            basename(preg_replace('|\?.*$|', '', $databaseURI)));
+
+        /* Convert some commonly encountered types to a fixed set of known
+         * service names: */
+        switch($database) {
+        case 'contacts':
+        case 'contact':
+        case 'card':
+        case 'scard':
+            return 'contacts';
+        case 'calendar':
+        case 'event':
+        case 'events':
+        case 'cal':
+        case 'scal':
+            return 'calendar';
+        case 'notes':
+        case 'memo':
+        case 'note':
+        case 'snote':
+            return 'notes';
+        case 'tasks':
+        case 'task':
+        case 'stask':
+            return 'tasks';
+        default:
+            return $database;
+        }
+    }
+
+    /**
+     * Extracts an HTTP GET like parameter from an URL.
+     *
+     * Example: <code>getParameter('test?q=1', 'q') == 1</code>
+     *
+     * @static
+     *
+     * @param string $url        The complete URL.
+     * @param string $parameter  The parameter name to extract.
+     * @param string $default    A default value to return if none has been
+     *                           provided in the URL.
+     */
+    function getParameter($url, $parameter, $default = null)
+    {
+        if (preg_match('|[&\?]' . $parameter . '=([^&]*)|', $url, $m)) {
+            return $m[1];
+        }
+        return $default;
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Backend/Horde.php b/framework/SyncML/SyncML/Backend/Horde.php
new file mode 100644 (file)
index 0000000..d5c6e0f
--- /dev/null
@@ -0,0 +1,983 @@
+<?php
+/**
+ * SyncML Backend for the Horde Application framework.
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did not
+ * receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/SyncML/SyncML/Backend/Horde.php,v 1.34 2009/07/17 21:00:13 slusarz Exp $
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Backend_Horde extends SyncML_Backend {
+
+    /**
+     * A PEAR DB instance.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Constructor.
+     *
+     * Initializes the logger.
+     *
+     * @param array $params  Any parameters the backend might need.
+     */
+    function SyncML_Backend_Horde($params)
+    {
+        parent::SyncML_Backend($params);
+
+        $this->_db = DB::connect($GLOBALS['conf']['sql']);
+
+        if (is_a($this->_db, 'PEAR_Error')) {
+            Horde::logMessage($this->_db, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        /* Set DB portability options. */
+        if (is_a($this->_db, 'DB_common')) {
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+        }
+    }
+
+    /**
+     * Sets the charset.
+     *
+     * All data passed to the backend uses this charset and data returned from
+     * the backend must use this charset, too.
+     *
+     * @param string $charset  A valid charset.
+     */
+    function setCharset($charset)
+    {
+        parent::setCharset($charset);
+
+        Horde_Nls::setCharset($this->getCharset());
+        Horde_String::setDefaultCharset($this->getCharset());
+    }
+
+    /**
+     * Sets the user used for this session.
+     *
+     * @param string $user  A user name.
+     */
+    function setUser($user)
+    {
+        parent::setUser($user);
+
+        if ($this->_backendMode == SYNCML_BACKENDMODE_TEST) {
+            /* After a session the user gets automatically logged out, so we
+             * have to login again. */
+            Horde_Auth::setAuth($this->_user, array());
+        }
+    }
+
+    /**
+     * Starts a PHP session.
+     *
+     * @param string $syncDeviceID  The device ID.
+     * @param string $session_id    The session ID to use.
+     * @param integer $backendMode  The backend mode, one of the
+     *                              SYNCML_BACKENDMODE_* constants.
+     */
+    function sessionStart($syncDeviceID, $sessionId,
+                          $backendMode = SYNCML_BACKENDMODE_SERVER)
+    {
+        $this->_backendMode = $backendMode;
+
+        /* Only the server needs to start a session. */
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            /* Reload the Horde SessionHandler if necessary. */
+            $GLOBALS['registry']->setupSessionHandler();
+        }
+
+        parent::sessionStart($syncDeviceID, $sessionId, $backendMode);
+    }
+
+    /**
+     * Returns entries that have been modified in the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param integer $from_ts     Start timestamp.
+     * @param integer $to_ts       Exclusive end timestamp. Not yet
+     *                             implemented.
+     * @param array &$adds         Output array: hash of adds suid => 0
+     * @param array &$mods         Output array: hash of modifications
+     *                             suid => cuid
+     * @param array &$dels         Output array: hash of deletions suid => cuid
+     *
+     * @return mixed  True on success or a PEAR_Error object.
+     */
+    function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
+                              &$dels)
+    {
+        global $registry;
+
+        $adds = $mods = $dels = array();
+        $database = $this->_normalize($databaseURI);
+        $slowsync = $from_ts == 0;
+
+        // Handle additions:
+        if ($slowsync) {
+            // Return all db entries directly rather than bother history. But
+            // first check if we only want to sync data from a given start
+            // date:
+            $start = trim(SyncML_Backend::getParameter($databaseURI, 'start'));
+            if (!empty($start)) {
+                if (strlen($start) == 4) {
+                    $start .= '0101000000';
+                } elseif (strlen($start) == 6) {
+                    $start .= '01000000';
+                } elseif (strlen($start) == 8) {
+                    $start .= '000000';
+                }
+                $start = new Horde_Date($start);
+                $this->logMessage('Slow-syncing all events starting from ' . (string)$start,
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $data = $registry->call(
+                    $database . '/list',
+                    array(SyncML_Backend::getParameter($databaseURI, 'source'),
+                          $start));
+            } else {
+                $data = $registry->call(
+                    $database . '/list',
+                    array(SyncML_Backend::getParameter($databaseURI, 'source')));
+            }
+        } else {
+            $data = $registry->call(
+                $database . '/listBy',
+                array('action' => 'add',
+                      'timestamp' => $from_ts,
+                      'source' => SyncML_Backend::getParameter($databaseURI,
+                                                               'source')));
+        }
+
+        if (is_a($data, 'PEAR_Error')) {
+            $this->logMessage("$database/list or $database/listBy failed while retrieving server additions:"
+                              . $data->getMessage(),
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $data;
+        }
+
+        $add_ts = array();
+        foreach ($data as $suid) {
+            // Only server needs to check for client sent entries:
+            if ($this->_backendMode != SYNCML_BACKENDMODE_SERVER) {
+                $adds[$suid] = 0;
+                continue;
+            }
+
+            if ($slowsync) {
+                // SlowSync: Ignore all entries where there already in a
+                // map entry.
+                $cuid = $this->_getCuid($database, $suid);
+                if ($cuid) {
+                    $this->logMessage(
+                        "Added to server from client during SlowSync: $suid ignored",
+                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    continue;
+                }
+            }
+            $add_ts[$suid] = $registry->call($database . '/getActionTimestamp',
+                                             array($suid, 'add', SyncML_Backend::getParameter($databaseURI, 'source')));
+            $sync_ts = $this->_getChangeTS($database, $suid);
+            if ($sync_ts && $sync_ts >= $add_ts[$suid]) {
+                // Change was done by us upon request of client.  Don't mirror
+                // that back to the client.
+                $this->logMessage("Added to server from client: $suid ignored",
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                continue;
+            }
+            $this->logMessage(
+                "Adding to client from db $database, server id $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $adds[$suid] = 0;
+        }
+
+        // On SlowSync: everything is sent as add, no need to send
+        // modifications or deletions. So we are finished here:
+        if ($slowsync) {
+            return true;
+        }
+
+        // Handle changes:
+        $data = $registry->call(
+            $database. '/listBy',
+            array('action' => 'modify',
+                  'timestamp' => $from_ts,
+                  'source' => SyncML_Backend::getParameter($databaseURI,'source')));
+        if (is_a($data, 'PEAR_Error')) {
+            $this->logMessage(
+                "$database/listBy failed while retrieving server modifications:"
+                . $data->getMessage(),
+                __FILE__, __LINE__, PEAR_LOG_WARNING);
+            return $data;
+        }
+
+        $mod_ts = array();
+        foreach ($data as $suid) {
+            // Check if the entry has been added after the last sync.
+            if (isset($adds[$suid])) {
+                continue;
+            }
+
+            // Only server needs to check for client sent entries and update
+            // map.
+            if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                $mod_ts[$suid] = $registry->call($database . '/getActionTimestamp',
+                                                 array($suid, 'modify', SyncML_Backend::getParameter($databaseURI,'source')));
+                $sync_ts = $this->_getChangeTS($database, $suid);
+                if ($sync_ts && $sync_ts >= $mod_ts[$suid]) {
+                    // Change was done by us upon request of client.  Don't
+                    // mirror that back to the client.
+                    $this->logMessage("Changed on server after sent from client: $suid ignored",
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    continue;
+                }
+                $cuid = $this->_getCuid($database, $suid);
+                if (!$cuid) {
+                    $this->logMessage(
+                        "Unable to create change for server id $suid: client id not found in map, adding instead.",
+                        __FILE__, __LINE__, PEAR_LOG_WARNING);
+                    $adds[$suid] = 0;
+                    continue;
+                } else {
+                    $mods[$suid] = $cuid;
+                }
+            } else {
+                $mods[$suid] = $suid;
+            }
+            $this->logMessage(
+                "Modifying on client from db $database, client id $cuid -> server id $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+
+        // Handle deletions.
+        $data = $registry->call(
+            $database . '/listBy',
+            array('action' => 'delete',
+                  'timestamp' => $from_ts,
+                  'source' => SyncML_Backend::getParameter($databaseURI, 'source')));
+
+        if (is_a($data, 'PEAR_Error')) {
+            $this->logMessage(
+                "$database/listBy failed while retrieving server deletions:"
+                . $data->getMessage(),
+                __FILE__, __LINE__, PEAR_LOG_WARNING);
+            return $data;
+        }
+
+        foreach ($data as $suid) {
+            // Only server needs to check for client sent entries.
+            if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                $suid_ts = $registry->call(
+                    $database . '/getActionTimestamp',
+                    array($suid,
+                          'delete',
+                          SyncML_Backend::getParameter($databaseURI,'source')));
+
+                // Check if the entry has been added or modified after the
+                // last sync.
+                if (isset($adds[$suid]) && $add_ts[$suid] < $suid_ts) {
+                    unset($adds[$suid]);
+                    continue;
+                }
+                if (isset($mods[$suid])) {
+                    unset($mods[$suid]);
+                }
+
+                $sync_ts = $this->_getChangeTS($database, $suid);
+                if ($sync_ts && $sync_ts >= $suid_ts) {
+                    // Change was done by us upon request of client.  Don't
+                    // mirror that back to the client.
+                    $this->logMessage("Deleted on server after request from client: $suid ignored",
+                                      __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    continue;
+                }
+                $cuid = $this->_getCuid($database, $suid);
+                if (!$cuid) {
+                    $this->logMessage(
+                        "Unable to create delete for server id $suid: client id not found in map",
+                        __FILE__, __LINE__, PEAR_LOG_WARNING);
+                    continue;
+                }
+                $dels[$suid] = $cuid;
+            } else {
+                $dels[$suid] = $suid;
+            }
+            $this->logMessage(
+                "Deleting on client from db $database, client id $cuid -> server id $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+
+        return true;
+    }
+
+    /**
+     * Retrieves an entry from the backend.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         Server unique id of the entry: for horde
+     *                             this is the guid.
+     * @param string $contentType  Content-Type: the MIME type in which the
+     *                             function should return the data.
+     *
+     * @return mixed  A string with the data entry or a PEAR_Error object.
+     */
+    function retrieveEntry($databaseURI, $suid, $contentType)
+    {
+        return $GLOBALS['registry']->call(
+            $this->_normalize($databaseURI) . '/export',
+            array('guid' => $suid, 'contentType' => $contentType));
+    }
+
+    /**
+     * Adds an entry into the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return array  PEAR_Error or suid (Horde guid) of new entry
+     */
+    function addEntry($databaseURI, $content, $contentType, $cuid = null)
+    {
+        global $registry;
+
+        $database = $this->_normalize($databaseURI);
+
+        $suid = $registry->call(
+            $database . '/import',
+            array($content,
+                  $contentType,
+                  SyncML_Backend::getParameter($databaseURI, 'source')));
+
+        if (!is_a($suid, 'PEAR_Error')) {
+            $this->logMessage(
+                "Added to server db $database client id $cuid -> server id $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $ts = $registry->call(
+                $database . '/getActionTimestamp',
+                array($suid,
+                      'add',
+                      SyncML_Backend::getParameter($databaseURI, 'source')));
+            if (!$ts) {
+                $this->logMessage(
+                    "Unable to find addition timestamp for server id $suid at $ts"
+                    , __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+            // Only server needs to do a cuid<->suid map
+            if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                $this->createUidMap($database, $cuid, $suid, $ts);
+            }
+        } else {
+            // Failed import. Maybe the entry is already there. Check if a
+            // guid is returned:
+            if ($suid->getDebugInfo()) {
+                $suid = $suid->getDebugInfo();
+                $this->logMessage(
+                    'Adding client entry to server: already exists with server id ' . $suid,
+                    __FILE__, __LINE__, PEAR_LOG_NOTICE);
+                if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                    $this->createUidMap($database, $cuid, $suid, 0);
+                }
+            }
+
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Replaces an entry in the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
+     */
+    function replaceEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        global $registry;
+
+        $database = $this->_normalize($databaseURI);
+
+        // Only server needs to do a cuid<->suid map
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $suid = $this->_getSuid($database, $cuid);
+        } else {
+            $suid = $cuid;
+        }
+
+        if (!$suid) {
+            return PEAR::raiseError("No map entry found for client id $cuid replacing on server");
+        }
+
+        // Entry exists: replace current one.
+        $ok = $registry->call($database . '/replace',
+                              array($suid, $content, $contentType));
+        if (is_a($ok, 'PEAR_Error')) {
+            return $ok;
+        }
+        $this->logMessage(
+            "Replaced in server db $database client id $cuid -> server id $suid",
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $ts = $registry->call(
+            $database . '/getActionTimestamp',
+            array($suid,
+                  'modify',
+                  SyncML_Backend::getParameter($databaseURI,'source')));
+        // Only server needs to do a cuid<->suid map
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $this->createUidMap($database, $cuid, $suid, $ts);
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Deletes an entry from the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     *
+     * @return boolean  True on success or false on failed (item not found).
+     */
+    function deleteEntry($databaseURI, $cuid)
+    {
+        global $registry;
+
+        $database = $this->_normalize($databaseURI);
+        // Find server ID for this entry:
+        // Only server needs to do a cuid<->suid map
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $suid = $this->_getSuid($database, $cuid);
+        } else {
+            $suid = $cuid;
+        }
+        if (is_a($suid, 'PEAR_Error')) {
+            return false;
+        }
+
+        $r = $registry->call($database. '/delete', array($suid));
+        if (is_a($r, 'PEAR_Error')) {
+            return false;
+        }
+
+        $this->logMessage(
+            "Deleted in server db $database client id $cuid -> server id $suid",
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $ts = $registry->call($database . '/getActionTimestamp',
+                              array($suid, 'delete'));
+        // We can't remove the mapping entry as we need to keep the timestamp
+        // information.
+        // Only server needs to do a cuid<->suid map
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $this->createUidMap($database, $cuid, $suid, $ts);
+        }
+
+        return true;
+    }
+
+    /**
+     * Authenticates the user at the backend.
+     *
+     * @param string $username    A user name.
+     * @param string $password    A password.
+     *
+     * @return boolean|string  The user name if authentication succeeded, false
+     *                         otherwise.
+     */
+    function _checkAuthentication($username, $password)
+    {
+        $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        return $auth->authenticate($username, array('password' => $password))
+            ? Horde_Auth::getAuth()
+            : false;
+    }
+
+    /**
+     * Sets a user as being authenticated at the backend.
+     *
+     * @abstract
+     *
+     * @param string $username    A user name.
+     * @param string $credData    Authentication data provided by <Cred><Data>
+     *                            in the <SyncHdr>.
+     *
+     * @return string  The user name.
+     */
+    function setAuthenticated($username, $credData)
+    {
+        Horde_Auth::setAuth($username, $credData);
+        return Horde_Auth::getAuth();
+    }
+
+    /**
+     * Stores Sync anchors after a successful synchronization to allow two-way
+     * synchronization next time.
+     *
+     * The backend has to store the parameters in its persistence engine
+     * where user, syncDeviceID and database are the keys while client and
+     * server anchor ar the payload. See readSyncAnchors() for retrieval.
+     *
+     * @param string $databaseURI       URI of database to sync. Like calendar,
+     *                                  tasks, contacts or notes. May include
+     *                                  optional parameters:
+     *                                  tasks?options=ignorecompleted.
+     * @param string $clientAnchorNext  The client anchor as sent by the
+     *                                  client.
+     * @param string $serverAnchorNext  The anchor as used internally by the
+     *                                  server.
+     */
+    function writeSyncAnchors($databaseURI, $clientAnchorNext,
+                              $serverAnchorNext)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        if (!$this->readSyncAnchors($databaseURI)) {
+            $query = 'INSERT INTO horde_syncml_anchors '
+                . '(syncml_clientanchor, syncml_serveranchor, '
+                . 'syncml_syncpartner, syncml_db, syncml_uid) '
+                . 'VALUES (?, ?, ?, ?, ?)';
+        } else {
+            $query = 'UPDATE horde_syncml_anchors '
+                . 'SET syncml_clientanchor = ?, syncml_serveranchor = ? '
+                . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+                . 'syncml_uid = ?';
+        }
+        $values = array($clientAnchorNext, $serverAnchorNext,
+                        $this->_syncDeviceID, $database, $this->_user);
+
+        $this->logMessage(
+            'SQL Query by SyncML_Backend_Horde::writeSyncAnchors(): '
+            . $query . ', values: ' . implode(', ', $values),
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $this->_db->query($query, $values);
+    }
+
+    /**
+     * Reads the previously written sync anchors from the database.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return mixed  Two-element array with client anchor and server anchor as
+     *                stored in previous writeSyncAnchor() calls. False if no
+     *                data found.
+     */
+    function readSyncAnchors($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $query = 'SELECT syncml_clientanchor, syncml_serveranchor '
+            . 'FROM horde_syncml_anchors '
+            . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+            . 'syncml_uid = ?';
+        $values = array($this->_syncDeviceID, $database, $this->_user);
+
+        $this->logMessage(
+            'SQL Query by SyncML_Backend_Horde::readSyncAnchors(): '
+            . $query . ', values: ' . implode(', ', $values),
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getRow($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns all previously written sync anchors for a user.
+     *
+     * @param string $user  A user name.
+     *
+     * @return array  A hash tree with all devices, databases and sync anchors
+     *                from the specified user.
+     */
+    function getUserAnchors($user)
+    {
+        $query = 'SELECT syncml_syncpartner, syncml_db, syncml_clientanchor, '
+            . 'syncml_serveranchor FROM horde_syncml_anchors '
+            . 'WHERE syncml_uid = ?';
+        $values = array($user);
+
+        $this->logMessage(
+            'SQL Query by SyncML_Backend_Horde::getUserAnchors(): '
+            . $query . ', values: ' . implode(', ', $values),
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getAssoc($query, false, $values,
+                                       DB_FETCHMODE_ASSOC, true);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Deletes previously written sync anchors for a user.
+     *
+     * If no device or database are specified, anchors for all devices and/or
+     * databases will be deleted.
+     *
+     * @param string $user      A user name.
+     * @param string $device    The ID of the client device.
+     * @param string $database  Normalized URI of database to delete. Like
+     *                          calendar, tasks, contacts or notes.
+     *
+     * @return array
+     */
+    function removeAnchor($user, $device = null, $database = null)
+    {
+        $query = 'DELETE FROM horde_syncml_anchors '
+            . 'WHERE syncml_uid = ?';
+        $values = array($user);
+        if (strlen($device)) {
+            $query .= ' AND syncml_syncpartner = ?';
+            $values[] = $device;
+        }
+        if (strlen($database)) {
+            $query .= ' AND syncml_db = ?';
+            $values[] = $database;
+        }
+
+        $this->logMessage(
+            'SQL Query by SyncML_Backend_Horde::removeAnchor(): '
+            . $query . ', values: ' . implode(', ', $values),
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Creates a map entry to map between server and client IDs.
+     *
+     * If an entry already exists, it is overwritten.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     * @param string $suid         Server ID of the entry.
+     * @param integer $timestamp   Optional timestamp. This can be used to
+     *                             'tag' changes made in the backend during the
+     *                             sync process. This allows to identify these,
+     *                             and ensure that these changes are not
+     *                             replicated back to the client (and thus
+     *                             duplicated). See key concept "Changes and
+     *                             timestamps".
+     */
+    function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Check if entry exists. If not insert, otherwise update.
+        if (!$this->_getSuid($databaseURI, $cuid)) {
+            $query = 'INSERT INTO horde_syncml_map '
+                . '(syncml_suid, syncml_timestamp, syncml_syncpartner, '
+                . 'syncml_db, syncml_uid, syncml_cuid) '
+                . 'VALUES (?, ?, ?, ?, ?, ?)';
+        } else {
+            $query = 'UPDATE horde_syncml_map '
+                . 'SET syncml_suid = ?, syncml_timestamp = ? '
+                . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+                . 'syncml_uid = ? AND syncml_cuid = ?';
+        }
+        $values = array($suid, (int)$timestamp, $this->_syncDeviceID,
+                        $database, $this->_user, $cuid);
+
+        $this->logMessage('SQL Query by SyncML_Backend_Horde::createUidMap(): '
+                          . $query . ', values: ' . implode(', ', $values),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        return true;
+    }
+
+    /**
+     * Retrieves the Server ID for a given Client ID from the map.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         The client ID.
+     *
+     * @return mixed  The server ID string or false if no entry is found.
+     */
+    function _getSuid($databaseURI, $cuid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $query = 'SELECT syncml_suid FROM horde_syncml_map '
+            . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+            . 'syncml_uid = ? AND syncml_cuid = ?';
+        $values = array($this->_syncDeviceID, $database, $this->_user, $cuid);
+
+        $this->logMessage('SQL Query by SyncML_Backend_Horde::_getSuid(): '
+                          . $query . ', values: ' . implode(', ', $values),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getOne($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Retrieves the Client ID for a given Server ID from the map.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         The server ID.
+     *
+     * @return mixed  The client ID string or false if no entry is found.
+     */
+    function _getCuid($databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $query = 'SELECT syncml_cuid FROM horde_syncml_map '
+            . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+            . 'syncml_uid = ? AND syncml_suid = ?';
+        $values = array($this->_syncDeviceID, $database, $this->_user, $suid);
+
+        $this->logMessage('SQL Query by SyncML_Backend_Horde::_getCuid(): '
+                          . $query . ', values: ' . implode(', ', $values),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getOne($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns a timestamp stored in the map for a given Server ID.
+     *
+     * The timestamp is the timestamp of the last change to this server ID
+     * that was done inside a sync session (as a result of a change received
+     * by the server). It's important to distinguish changes in the backend a)
+     * made by the user during normal operation and b) changes made by SyncML
+     * to reflect client updates.  When the server is sending its changes it
+     * is only allowed to send type a). However the history feature in the
+     * backend my not know if a change is of type a) or type b). So the
+     * timestamp is used to differentiate between the two.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         The server ID.
+     *
+     * @return mixed  The previously stored timestamp or false if no entry is
+     *                found.
+     */
+    function _getChangeTS($databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $query = 'SELECT syncml_timestamp FROM horde_syncml_map '
+            . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+            . 'syncml_uid = ? AND syncml_suid = ?';
+        $values = array($this->_syncDeviceID, $database, $this->_user, $suid);
+
+        $this->logMessage('SQL Query by SyncML_Backend_Horde::_getChangeTS(): '
+                          . $query . ', values: ' . implode(', ', $values),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->getOne($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Erases all mapping entries for one combination of user, device ID.
+     *
+     * This is used during SlowSync so that we really sync everything properly
+     * and no old mapping entries remain.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     */
+    function eraseMap($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $query = 'DELETE FROM horde_syncml_map '
+            . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
+            . 'syncml_uid = ?';
+        $values = array($this->_syncDeviceID, $database, $this->_user);
+
+        $this->logMessage('SQL Query by SyncML_Backend_Horde::eraseMap(): '
+                          . $query . ', values: ' . implode(', ', $values),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($query, $values);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Logs a message in the backend.
+     *
+     * @param mixed $message     Either a string or a PEAR_Error object.
+     * @param string $file       What file was the log function called from
+     *                           (e.g. __FILE__)?
+     * @param integer $line      What line was the log function called from
+     *                           (e.g. __LINE__)?
+     * @param integer $priority  The priority of the message. One of:
+     *                           - PEAR_LOG_EMERG
+     *                           - PEAR_LOG_ALERT
+     *                           - PEAR_LOG_CRIT
+     *                           - PEAR_LOG_ERR
+     *                           - PEAR_LOG_WARNING
+     *                           - PEAR_LOG_NOTICE
+     *                           - PEAR_LOG_INFO
+     *                           - PEAR_LOG_DEBUG
+     */
+    function logMessage($message, $file, $line, $priority = PEAR_LOG_INFO)
+    {
+        // Internal logging to $this->_logtext.
+        parent::logMessage($message, $file, $line, $priority);
+
+        // Logging to Horde log:
+        Horde::logMessage($message, $file, $line, $priority);
+    }
+
+    /**
+     * Creates a clean test environment in the backend.
+     *
+     * Ensures there's a user with the given credentials and an empty data
+     * store.
+     *
+     * @param string $user This user accout has to be created in the backend.
+     * @param string $pwd  The password for user $user.
+     *
+     * @throws Horde_Exception
+     */
+    function testSetup($user, $pwd)
+    {
+        $this->_user = $user;
+        if (empty($pwd)) {
+            $pwd = rand() . rand();
+        }
+
+        /* Get an Auth object. */
+        $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+
+        /* Make this user an admin for the time beeing to allow deletion of
+         * user data. */
+        $GLOBALS['conf']['auth']['admins'][] = $user;
+
+        /* Always remove test user first. */
+        if ($auth->exists($user)) {
+            /* We need to be logged in to call removeUserData, otherwise we
+             * run into permission issues. */
+            Horde_Auth::setAuth($user, array());
+            try {
+                Horde_Auth::removeUserData($user);
+            } catch (Horde_Exception $e) {
+                // TODO
+            }
+            $auth->removeUser($user);
+        }
+
+        $auth->addUser($user, array('password' => $pwd));
+    }
+
+    /**
+     * Prepares the test start.
+     *
+     * @param string $user This user accout has to be created in the backend.
+     */
+    function testStart($user)
+    {
+        $this->_user = $user;
+
+        /* Make this user an admin for the time beeing to allow deletion of
+         * user data. */
+        $GLOBALS['conf']['auth']['admins'][] = $user;
+
+        Horde_Auth::setAuth($user, array());
+    }
+
+    /**
+     * Tears down the test environment after the test is run.
+     *
+     * Should remove the testuser created during testSetup and all its data.
+     */
+    function testTearDown()
+    {
+        /* Get an Auth object. */
+        try {
+            $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        } catch (Horde_Exception $e) {
+            // TODO
+        }
+
+        /* We need to be logged in to call removeUserData, otherwise we run
+         * into permission issues. */
+        Horde_Auth::setAuth($this->_user, array());
+
+        print "\nCleaning up: removing test user data and test user...";
+        Horde_Auth::removeUserData($this->_user);
+        $auth->removeUser($this->_user);
+
+        print "OK\n";
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Backend/Sql.php b/framework/SyncML/SyncML/Backend/Sql.php
new file mode 100644 (file)
index 0000000..7a01962
--- /dev/null
@@ -0,0 +1,1176 @@
+<?php
+/**
+ * Generic SQL based SyncML Backend.
+ *
+ * This can be used as a starting point for a custom backend implementation.
+ *
+ * $Horde: framework/SyncML/SyncML/Backend/Sql.php,v 1.13 2009/04/05 16:42:05 jan Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+
+require_once 'MDB2.php';
+
+/*
+ * The SQL Database must contain five tables as created by the following SQL
+ * script:
+ *
+ * CREATE DATABASE syncml;
+ *
+ * USE syncml;
+ *
+ * CREATE TABLE syncml_data(
+ *     syncml_id            VARCHAR(255),
+ *     syncml_db            VARCHAR(255),
+ *     syncml_uid           VARCHAR(255),
+ *     syncml_data          TEXT,
+ *     syncml_contenttype   VARCHAR(255),
+ *     syncml_created_ts    INTEGER,
+ *     syncml_modified_ts   INTEGER
+ * );
+ *
+ * CREATE TABLE syncml_map(
+ *     syncml_syncpartner VARCHAR(255),
+ *     syncml_db          VARCHAR(255),
+ *     syncml_uid         VARCHAR(255),
+ *     syncml_cuid        VARCHAR(255),
+ *     syncml_suid        VARCHAR(255),
+ *     syncml_timestamp   INTEGER
+ * );
+ *
+ * CREATE INDEX syncml_syncpartner_idx ON syncml_map (syncml_syncpartner);
+ * CREATE INDEX syncml_db_idx ON syncml_map (syncml_db);
+ * CREATE INDEX syncml_uid_idx ON syncml_map (syncml_uid);
+ * CREATE INDEX syncml_cuid_idx ON syncml_map (syncml_cuid);
+ * CREATE INDEX syncml_suid_idx ON syncml_map (syncml_suid);
+ *
+ * CREATE TABLE syncml_anchors(
+ *     syncml_syncpartner   VARCHAR(255),
+ *     syncml_db            VARCHAR(255),
+ *     syncml_uid           VARCHAR(255),
+ *     syncml_clientanchor  VARCHAR(255),
+ *     syncml_serveranchor  VARCHAR(255)
+ * );
+ *
+ * CREATE TABLE syncml_suidlist(
+ *     syncml_syncpartner    VARCHAR(255),
+ *     syncml_db             VARCHAR(255),
+ *     syncml_uid            VARCHAR(255),
+ *     syncml_suid           VARCHAR(255)
+ * );
+ *
+ * CREATE TABLE syncml_uids(
+ *     syncml_uid      VARCHAR(255),
+ *     syncml_password VARCHAR(255)
+ * );
+ */
+
+/**
+ */
+class SyncML_Backend_Sql extends SyncML_Backend {
+
+    /**
+     * A PEAR MDB2 instance.
+     *
+     * @var MDB2
+     */
+    var $_db;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  A hash with parameters. In addition to those
+     *                       supported by the SyncML_Backend class one more
+     *                       parameter is required for the database connection:
+     *                       'dsn' => connection DSN.
+     */
+    function SyncML_Backend_Sql($params)
+    {
+        parent::SyncML_Backend($params);
+
+        $this ->_db = &MDB2::connect($params['dsn']);
+        if (is_a($this->_db, 'PEAR_Error')) {
+            $this->logMessage($this->_db,
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+    }
+
+    /**
+     * Returns whether a database URI is valid to be synced with this backend.
+     *
+     * @param string $databaseURI  URI of a database. Like calendar, tasks,
+     *                             contacts or notes. May include optional
+     *                             parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return boolean  True if a valid URI.
+     */
+    function isValidDatabaseURI($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        switch($database) {
+        case 'tasks';
+        case 'calendar';
+        case 'notes';
+        case 'contacts';
+        case 'events':
+        case 'memo':
+            return true;
+
+        default:
+            $this->logMessage('Invalid database ' . $database
+                              . '. Try tasks, calendar, notes or contacts.',
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+    }
+
+    /**
+     * Returns entries that have been modified in the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param integer $from_ts     Start timestamp.
+     * @param integer $to_ts       Exclusive end timestamp. Not yet
+     *                             implemented.
+     * @param array &$adds         Output array: hash of adds suid => 0
+     * @param array &$mods         Output array: hash of modifications
+     *                             suid => cuid
+     * @param array &$dels         Output array: hash of deletions suid => cuid
+     *
+     * @return mixed  True on success or a PEAR_Error object.
+     */
+    function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
+                              &$dels)
+    {
+        $database = $this->_normalize($databaseURI);
+        $adds = $mods = $dels = array();
+
+        // Handle additions:
+        $data = $this->_db->queryAll(
+            'SELECT syncml_id, syncml_created_ts from syncml_data '
+            . 'WHERE syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_created_ts >= '
+            . $this->_db->quote($from_ts, 'integer')
+            . ' AND syncml_created_ts < '
+            . $this->_db->quote($to_ts, 'integer'));
+        if ($this->_checkForError($data)) {
+            return $data;
+        }
+
+        foreach ($data as $d) {
+            $suid = $d[0];
+            $suid_ts = $d[1];
+            $sync_ts = $this->_getChangeTS($databaseURI, $suid);
+            if ($sync_ts && $sync_ts >= $suid_ts) {
+                // Change was done by us upon request of client, don't mirror
+                // that back to the client.
+                $this->logMessage("Added to server from client: $suid ignored",
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                continue;
+            }
+            $adds[$suid] = 0;
+        }
+
+        // Only compile changes on delta sync:
+        if ($from_ts > 0) {
+            // Handle replaces. We might get IDs that are already in the adds
+            // array but that's ok: The calling code takes care to ignore
+            // these.
+            $data = $this->_db->queryAll(
+                'SELECT syncml_id, syncml_modified_ts from syncml_data '
+                .'WHERE syncml_db = '
+                . $this->_db->quote($database, 'text')
+                . ' AND syncml_uid = '
+                . $this->_db->quote($this->_user, 'text')
+                . ' AND syncml_modified_ts >= '
+                . $this->_db->quote($from_ts, 'integer')
+                . ' AND syncml_modified_ts < '
+                . $this->_db->quote($to_ts, 'integer'));
+            if ($this->_checkForError($data)) {
+                return $data;
+            }
+
+            foreach($data as $d) {
+                // Only the server needs to check the change timestamp do
+                // identify client-sent changes.
+                if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                    $suid = $d[0];
+                    $suid_ts = $d[1];
+                    $sync_ts = $this->_getChangeTS($databaseURI, $suid);
+                    if ($sync_ts && $sync_ts >= $suid_ts) {
+                        // Change was done by us upon request of client, don't
+                        // mirror that back to the client.
+                        $this->logMessage(
+                            "Changed on server after sent from client: $suid ignored",
+                            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                        continue;
+                    }
+                    $mods[$suid] = $this->_getCuid($databaseURI, $suid);
+                } else {
+                    $mods[$d[0]] = $d[0];
+                }
+            }
+        }
+
+        // Handle deletions:
+        // We assume stupid a backend datastore (syncml_data) where deleted
+        // items are simply "gone" from the datastore. So we need to do our
+        // own bookkeeping to identify entries that have been deleted since
+        // the last sync run.
+        // This is done by the _trackDeless() helper function: we feed it with
+        // a current list of all suids and get the ones missing (and thus
+        // deleted) in return.
+        $data = $this->_db->queryCol(
+            'SELECT syncml_id from syncml_data WHERE syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text'));
+        if ($this->_checkForError($data)) {
+            return $data;
+        }
+
+        // Get deleted items and store current items:
+        // Only use the deleted information on delta sync. On initial slowsync
+        // we just need to call _trackDeletes() once to init the list.
+        $data = $this->_trackDeletes($databaseURI, $data);
+        if ($this->_checkForError($data)) {
+            return $data;
+        }
+
+        if ($from_ts > 0) {
+            foreach($data as $suid) {
+                // Only the server needs to handle the cuid suid map:
+                if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                    $dels[$suid] = $this->_getCuid($databaseURI, $suid);
+                } else {
+                    $dels[$suid] = $suid;
+                }
+            }
+        }
+    }
+
+    /**
+     * Retrieves an entry from the backend.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         Server unique id of the entry: for horde
+     *                             this is the guid.
+     * @param string $contentType  Content-Type: the MIME type in which the
+     *                             function should return the data.
+     *
+     * @return mixed  A string with the data entry or a PEAR_Error object.
+     */
+    function retrieveEntry($databaseURI, $suid, $contentType)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        return $this->_db->queryOne(
+            'SELECT syncml_data from syncml_data '
+            . 'WHERE syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_id = '
+            . $this->_db->quote($suid, 'text'));
+    }
+
+    /**
+     * Adds an entry into the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return array  PEAR_Error or suid (Horde guid) of new entry
+     */
+    function addEntry($databaseURI, $content, $contentType, $cuid = null)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Generate an id (suid). It's also possible to use a database
+        // generated primary key here.
+        $suid = $this->_generateID();
+        $created_ts = $this->getCurrentTimeStamp();
+
+        $r = $this->_db->exec(
+            'INSERT INTO syncml_data (syncml_id, syncml_db, syncml_uid, '
+            . 'syncml_data, syncml_contenttype,  syncml_created_ts, '
+            . 'syncml_modified_ts) VALUES ('
+            . $this->_db->quote($suid, 'text') . ','
+            . $this->_db->quote($database, 'text') . ','
+            . $this->_db->quote($this->_user, 'text') . ','
+            . $this->_db->quote($content, 'text') . ','
+            . $this->_db->quote($contentType, 'text') . ','
+            . $this->_db->quote($created_ts, 'integer') . ','
+            . $this->_db->quote($created_ts, 'integer')
+            . ')');
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        // Only the server needs to handle the cuid suid map:
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+           $this->createUidMap($databaseURI, $cuid, $suid, $created_ts);
+        }
+    }
+
+    /**
+     * Replaces an entry in the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $cuid         Client ID of this entry.
+     *
+     * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
+     */
+    function replaceEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $suid = $this->_getSuid($databaseURI, $cuid);
+        } else {
+            $suid = $cuid;
+        }
+
+        if ($suid) {
+            // Entry exists: replace current one.
+            $modified_ts = $this->getCurrentTimeStamp();
+            $r = $this->_db->exec(
+                'UPDATE syncml_data '
+                . 'SET syncml_modified_ts = '
+                . $this->_db->quote($modified_ts, 'integer')
+                . ', syncml_data = '
+                . $this->_db->quote($content, 'text')
+                . ', syncml_contenttype = '
+                . $this->_db->quote($contentType, 'text')
+                . 'WHERE syncml_db = '
+                . $this->_db->quote($database, 'text')
+                . ' AND syncml_uid = '
+                . $this->_db->quote($this->_user, 'text')
+                . ' AND syncml_id = '
+                . $this->_db->quote($suid, 'text'));
+            if ($this->_checkForError($r)) {
+                return $r;
+            }
+
+            // Only the server needs to keep the map:
+            if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+                $this->createUidMap($databaseURI, $cuid, $suid, $modified_ts);
+            }
+        } else {
+            return PEAR::raiseError("No map entry found for client id $cuid replacing on server");
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Deletes an entry from the server database.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     *
+     * @return boolean  True on success or false on failed (item not found).
+     */
+    function deleteEntry($databaseURI, $cuid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Find ID for this entry:
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $suid = $this->_getSuid($databaseURI, $cuid);
+        } else {
+            $suid = $cuid;
+        }
+
+        if (!is_a($suid, 'PEAR_Error')) {
+            // A clever backend datastore would store some information about a
+            // deletion so this information can be extracted from the history.
+            // However we do a "stupid" datastore here where deleted items are
+            // simply gone. This allows us to illustrate the _trackDeletes()
+            // bookkeeping mechanism.
+            $r = $this->_db->queryOne(
+                'DELETE FROM syncml_data '
+                . ' WHERE syncml_db = '
+                . $this->_db->quote($database, 'text')
+                . ' AND syncml_uid = '
+                . $this->_db->quote($this->_user, 'text')
+                . ' AND syncml_id = '
+                . $this->_db->quote($suid, 'text'));
+            if ($this->_checkForError($r)) {
+                return $r;
+            }
+
+            // Deleted bookkeeping is required for server and client, but not
+            // for test mode:
+            if ($this->_backendMode != SYNCML_BACKENDMODE_TEST) {
+                $this->_removeFromSuidList($databaseURI, $suid);
+            }
+
+            // @todo: delete from map!
+        } else {
+            return false;
+        }
+
+        if (is_a($r, 'PEAR_Error')) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Authenticates the user at the backend.
+     *
+     * @param string $username    A user name.
+     * @param string $password    A password.
+     *
+     * @return boolean|string  The user name if authentication succeeded, false
+     *                         otherwise.
+     */
+    function _checkAuthentication($username, $password)
+    {
+            // Empty passwords result in errors for some authentication
+            // backends, don't call the backend in this case.
+            if ($pwd === '') {
+                return false;
+            }
+            $r = $this->_db->queryOne(
+                'SELECT syncml_uid FROM syncml_uids'
+                . ' WHERE syncml_uid = '
+                . $this->_db->quote($username, 'text')
+                . ' AND syncml_password = '
+                . $this->_db->quote($pwd, 'text'));
+            $this->_checkForError($r);
+
+            if ($r === $username) {
+                return $username;
+            }
+            return false;
+    }
+
+    /**
+     * Sets a user as being authenticated at the backend.
+     *
+     * @abstract
+     *
+     * @param string $username    A user name.
+     * @param string $credData    Authentication data provided by <Cred><Data>
+     *                            in the <SyncHdr>.
+     *
+     * @return string  The user name.
+     */
+    function setAuthenticated($username, $credData)
+    {
+        return $username;
+    }
+
+    /**
+     * Stores Sync anchors after a successful synchronization to allow two-way
+     * synchronization next time.
+     *
+     * The backend has to store the parameters in its persistence engine
+     * where user, syncDeviceID and database are the keys while client and
+     * server anchor ar the payload. See readSyncAnchors() for retrieval.
+     *
+     * @param string $databaseURI       URI of database to sync. Like calendar,
+     *                                  tasks, contacts or notes. May include
+     *                                  optional parameters:
+     *                                  tasks?options=ignorecompleted.
+     * @param string $clientAnchorNext  The client anchor as sent by the
+     *                                  client.
+     * @param string $serverAnchorNext  The anchor as used internally by the
+     *                                  server.
+     */
+    function writeSyncAnchors($databaseURI, $clientAnchorNext,
+                              $serverAnchorNext)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Check if entry exists. If not insert, otherwise update.
+        if (!$this->readSyncAnchors($databaseURI)) {
+            $r = $this->_db->exec(
+                'INSERT INTO syncml_anchors (syncml_syncpartner, '
+                . 'syncml_db,syncml_uid, syncml_clientanchor, '
+                . 'syncml_serveranchor) VALUES ('
+                . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
+                . $this->_db->quote($database, 'text') . ', '
+                . $this->_db->quote($this->_user, 'text') . ', '
+                . $this->_db->quote($clientAnchorNext, 'text') . ', '
+                . $this->_db->quote($serverAnchorNext, 'text')
+                . ')');
+        } else {
+            $r = $this->_db->exec(
+                'UPDATE syncml_anchors '
+                . ' SET syncml_clientanchor = '
+                . $this->_db->quote($clientAnchorNext, 'text')
+                . ', syncml_serveranchor = '
+                . $this->_db->quote($serverAnchorNext, 'text')
+                . ' WHERE syncml_syncpartner = '
+                . $this->_db->quote($this->_syncDeviceID, 'text')
+                . ' AND syncml_db = '
+                . $this->_db->quote($database, 'text')
+                . ' AND syncml_uid = '
+                . $this->_db->quote($this->_user, 'text'));
+        }
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        return true;
+    }
+
+    /**
+     * Reads the previously written sync anchors from the database.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     *
+     * @return mixed  Two-element array with client anchor and server anchor as
+     *                stored in previous writeSyncAnchor() calls. False if no
+     *                data found.
+     */
+    function readSyncAnchors($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->queryRow(
+            'SELECT syncml_clientanchor, syncml_serveranchor '
+            . 'FROM syncml_anchors WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text'));
+        $this->_checkForError($r);
+
+        if (!is_array($r)) {
+            return false;
+        }
+
+        return array($r[0], $r[1]);
+    }
+
+    /**
+     * Creates a map entry to map between server and client IDs.
+     *
+     * If an entry already exists, it is overwritten.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of the entry.
+     * @param string $suid         Server ID of the entry.
+     * @param integer $timestamp   Optional timestamp. This can be used to
+     *                             'tag' changes made in the backend during the
+     *                             sync process. This allows to identify these,
+     *                             and ensure that these changes are not
+     *                             replicated back to the client (and thus
+     *                             duplicated). See key concept "Changes and
+     *                             timestamps".
+     */
+    function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Check if entry exists. If not insert, otherwise update.
+        if (!$this->_getSuid($databaseURI, $cuid)) {
+            $r = $this->_db->exec(
+                'INSERT INTO syncml_map (syncml_syncpartner, '
+                . 'syncml_db, syncml_uid, syncml_cuid, syncml_suid, '
+                . 'syncml_timestamp) VALUES ('
+                . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
+                . $this->_db->quote($database, 'text') . ', '
+                . $this->_db->quote($this->_user, 'text') . ', '
+                . $this->_db->quote($cuid, 'text') . ', '
+                . $this->_db->quote($suid, 'text') . ', '
+                . $this->_db->quote($timestamp, 'integer')
+                . ')');
+        } else {
+            $r = $this->_db->exec(
+                'UPDATE syncml_map SET syncml_suid = '
+                . $this->_db->quote($suid, 'text')
+                . ', syncml_timestamp = '
+                . $this->_db->quote($timestamp, 'text')
+                . ' WHERE syncml_syncpartner = '
+                . $this->_db->quote($this->_syncDeviceID, 'text')
+                . ' AND syncml_db = '
+                . $this->_db->quote($database, 'text')
+                . ' AND syncml_uid = '
+                . $this->_db->quote($this->_user, 'text')
+                . ' AND syncml_cuid = '
+                . $this->_db->quote($cuid, 'text'));
+        }
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        return true;
+    }
+
+    /**
+     * Retrieves the Server ID for a given Client ID from the map.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         The client ID.
+     *
+     * @return mixed  The server ID string or false if no entry is found.
+     */
+    function _getSuid($databaseURI, $cuid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->queryOne(
+            'SELECT syncml_suid FROM syncml_map '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_cuid = '
+            . $this->_db->quote($cuid, 'text'));
+        $this->_checkForError($r);
+
+        if (!empty($r)) {
+            return $r;
+        }
+
+        return false;
+    }
+
+    /**
+     * Retrieves the Client ID for a given Server ID from the map.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         The server ID.
+     *
+     * @return mixed  The client ID string or false if no entry is found.
+     */
+    function _getCuid($databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->queryOne(
+            'SELECT syncml_cuid FROM syncml_map '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_suid = '
+            . $this->_db->quote($suid, 'text'));
+
+        $this->_checkForError($r);
+
+        if (!empty($r)) {
+            return $r;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a timestamp stored in the map for a given Server ID.
+     *
+     * The timestamp is the timestamp of the last change to this server ID
+     * that was done inside a sync session (as a result of a change received
+     * by the server). It's important to distinguish changes in the backend a)
+     * made by the user during normal operation and b) changes made by SyncML
+     * to reflect client updates.  When the server is sending its changes it
+     * is only allowed to send type a). However the history feature in the
+     * backend my not know if a change is of type a) or type b). So the
+     * timestamp is used to differentiate between the two.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         The server ID.
+     *
+     * @return mixed  The previously stored timestamp or false if no entry is
+     *                found.
+     */
+    function _getChangeTS($databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->queryOne(
+            'SELECT syncml_timestamp FROM syncml_map '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_suid = '
+            . $this->_db->quote($suid, 'text'));
+        $this->_checkForError($r);
+
+        if (!empty($r)) {
+            return $r;
+        }
+
+        return false;
+    }
+
+    /**
+     * Erases all mapping entries for one combination of user, device ID.
+     *
+     * This is used during SlowSync so that we really sync everything properly
+     * and no old mapping entries remain.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     */
+    function eraseMap($databaseURI)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->exec(
+            'DELETE FROM syncml_map '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text'));
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        $r = $this->_db->exec(
+            'DELETE FROM syncml_suidlist '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text'));
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        return true;
+    }
+
+    /**
+     * Cleanup function called after all message processing is finished.
+     *
+     * Allows for things like closing databases or flushing logs.  When
+     * running in test mode, tearDown() must be called rather than close.
+     */
+    function close()
+    {
+        parent::close();
+        $this->_db->disconnect();
+    }
+
+    /**
+     * Generates a unique ID used as suid
+     *
+     * @return string  A unique ID.
+     */
+    function _generateID()
+    {
+        return date('YmdHis') . '.'
+            . substr(str_pad(base_convert(microtime(), 10, 36),
+                             16,
+                             uniqid(mt_rand()),
+                             STR_PAD_LEFT),
+                     -16)
+            . '@'
+            . (!empty($_SERVER['SERVER_NAME'])
+               ? $_SERVER['SERVER_NAME']
+               : 'localhost');
+    }
+
+    /**
+     * Checks if the parameter is a PEAR_Error object and if so logs the
+     * error.
+     *
+     * @param mixed $o  An object or value to check.
+     *
+     * @return mixed  The error object if an error has been passed or false if
+     *                no error has been passed.
+     */
+    function _checkForError($o)
+    {
+        if (is_a($o, 'PEAR_Error')) {
+            $this->logMessage($o);
+            return $o;
+        }
+        return false;
+    }
+
+    /**
+     * Returns a list of item IDs that have been deleted since the last sync
+     * run and stores a complete list of IDs for next sync run.
+     *
+     * Some backend datastores don't keep information about deleted entries.
+     * So we have to create a workaround that finds out what entries have been
+     * deleted since the last sync run. This method provides this
+     * functionality: it is called with a list of all IDs currently in the
+     * database. It then compares this list with its own previously stored
+     * list of IDs to identify those missing (and thus deleted). The passed
+     * list is then stored for the next invocation.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param array $currentSuids  Array of all SUIDs (primary keys) currently
+     *                             in the server datastore.
+     *
+     * @return array  Array of all entries that have been deleted since the
+     *                last call.
+     */
+    function _trackDeletes($databaseURI, $currentSuids)
+    {
+        $database = $this->_normalize($databaseURI);
+        if (!is_array($currentSuids)) {
+            $currentSuids = array();
+        }
+
+        $this->logMessage('_trackDeletes() with ' . count($currentSuids)
+                          . ' current ids',
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $r = $this->_db->queryCol(
+            'SELECT syncml_suid FROM syncml_suidlist '
+            . ' WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text'));
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        $this->logMessage('_trackDeletes() found ' . count($r)
+                          . ' items in prevlist',
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // Convert to hash with suid as key.
+        if (is_array($r)) {
+            $prevSuids = array_flip($r);
+        } else {
+            $prevSuids = array();
+        }
+
+        foreach ($currentSuids as $suid) {
+            if (isset($prevSuids[$suid])) {
+                // Entry is there now and in $prevSuids. Unset in $prevSuids
+                // array so we end up with only those in $prevSuids that are
+                // no longer there now.
+                unset($prevSuids[$suid]);
+            } else {
+                // Entry is there now but not in $prevSuids. New entry, store
+                // in syncml_suidlist
+                $r = $this->_db->exec(
+                    'INSERT INTO syncml_suidlist '
+                    . ' (syncml_syncpartner, syncml_db, syncml_uid, '
+                    . 'syncml_suid) VALUES ('
+                    . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
+                    . $this->_db->quote($database, 'text') . ', '
+                    . $this->_db->quote($this->_user, 'text') . ', '
+                    . $this->_db->quote($suid, 'text')
+                    . ')');
+                if ($this->_checkForError($r)) {
+                    return $r;
+                }
+            }
+        }
+
+        // $prevSuids now contains the deleted suids. Remove those from
+        // syncml_suidlist so we have a current list of all existing suids.
+        foreach ($prevSuids as $suid => $cuid) {
+            $r = $this->_removeFromSuidList($databaseURI, $suid);
+        }
+
+        $this->logMessage('_trackDeletes() with ' . count($prevSuids)
+                          . ' deleted items',
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return array_keys($prevSuids);
+    }
+
+    /**
+     * Removes a suid from the suidlist.
+     *
+     * Called by _trackDeletes() when updating the suidlist and deleteEntry()
+     * when removing an entry due to a client request.
+     *
+     * @param string $databaseURI  URI of database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param array $suid          The suid to remove from the list.
+     */
+    function _removeFromSuidList($databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $this->logMessage('_removeFromSuidList(): item ' . $suid,
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $r = $this->_db->queryCol(
+            'DELETE FROM syncml_suidlist '
+            . 'WHERE syncml_syncpartner = '
+            . $this->_db->quote($this->_syncDeviceID, 'text')
+            . ' AND syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($this->_user, 'text')
+            . ' AND syncml_suid = '
+            . $this->_db->quote($suid, 'text'));
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        $this->logMessage('_removeFromSuidList(): result ' . implode('!', $r),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return true;
+    }
+
+    /**
+     * Creates a clean test environment in the backend.
+     *
+     * Ensures there's a user with the given credentials and an empty data
+     * store.
+     *
+     * @param string $user This user accout has to be created in the backend.
+     * @param string $pwd  The password for user $user.
+     */
+    function testSetup($user, $pwd)
+    {
+        $this->_user = $user;
+        $this->_cleanUser($user);
+        $this->_backend->_user = $user;
+
+        $r = $this->_db->exec(
+            'INSERT INTO syncml_uids (syncml_uid, syncml_password)'
+            . ' VALUES ('
+            . $this->_db->quote($user, 'text') . ', '
+            . $this->_db->quote($pwd, 'text') . ')');
+        $this->_checkForError($r);
+    }
+
+    /**
+     * Prepares the test start.
+     *
+     * @param string $user This user accout has to be created in the backend.
+     */
+    function testStart($user)
+    {
+        $this->_user = $user;
+        $this->_backend->_user = $user;
+    }
+
+    /**
+     * Tears down the test environment after the test is run.
+     *
+     * Should remove the testuser created during testSetup and all its data.
+     */
+    function testTearDown()
+    {
+        $this->_cleanUser($this->_user);
+        $this->_db->disconnect();
+    }
+
+    /* Database access functions. The following methods are not part of the
+     * backend API. They are here to illustrate how a backend application
+     * (like a web calendar) has to modify the data with respect to the
+     * history. There are three functions:
+     * addEntry_backend(), replaceEntry_backend(), deleteEntry_backend().
+     * They are very similar to the API methods above, but don't use cuids or
+     * syncDeviceIDs as these are only relevant for syncing. */
+
+    /**
+     * Adds an entry into the server database.
+     *
+     * @param string $user         The username to use. Not strictly necessery
+     *                             to store this, but it helps for the test
+     *                             environment to clean up all entries for a
+     *                             test user.
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     *
+     * @return array  PEAR_Error or suid of new entry.
+     */
+    function addEntry_backend($user, $databaseURI, $content, $contentType)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        // Generate an id (suid). It's also possible to use a database
+        // generated primary key here. */
+        $suid = $this->_generateID();
+
+        $created_ts = $this->getCurrentTimeStamp();
+        $r = $this->_db->exec(
+            'INSERT INTO syncml_data (syncml_id, syncml_db, syncml_uid, '
+            . 'syncml_data, syncml_contenttype, syncml_created_ts, '
+            . 'syncml_modified_ts) VALUES ('
+            . $this->_db->quote($suid, 'text') . ', '
+            . $this->_db->quote($database, 'text') . ', '
+            . $this->_db->quote($user, 'text') . ', '
+            . $this->_db->quote($content, 'text') . ', '
+            . $this->_db->quote($contentType, 'text') . ', '
+            . $this->_db->quote($created_ts, 'integer') . ', '
+            . $this->_db->quote($created_ts, 'integer')
+            . ')');
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Replaces an entry in the server database.
+     *
+     * @param string $user         The username to use. Not strictly necessery
+     *                             to store this but, it helps for the test
+     *                             environment to clean up all entries for a
+     *                             test user.
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $content      The actual data.
+     * @param string $contentType  MIME type of the content.
+     * @param string $suid         Server ID of this entry.
+     *
+     * @return string  PEAR_Error or suid of modified entry.
+     */
+    function replaceEntry_backend($user, $databaseURI, $content, $contentType,
+                                  $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+        $modified_ts = $this->getCurrentTimeStamp();
+
+        // Entry exists: replace current one.
+        $r = $this->_db->exec(
+            'UPDATE syncml_data '
+            . 'SET syncml_modified_ts = '
+            . $this->_db->quote($modified_ts, 'integer')
+            . ',syncml_data = '
+            . $this->_db->quote($content, 'text')
+            . ',syncml_contenttype = '
+            . $this->_db->quote($contentType, 'text')
+            . 'WHERE syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($user, 'text')
+            . ' AND syncml_id = '
+            . $this->_db->quote($suid, 'text'));
+        if ($this->_checkForError($r)) {
+            return $r;
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Deletes an entry from the server database.
+     *
+     * @param string $user         The username to use. Not strictly necessery
+     *                             to store this, but it helps for the test
+     *                             environment to clean up all entries for a
+     *                             test user.
+     * @param string $databaseURI  URI of Database to sync. Like calendar,
+     *                             tasks, contacts or notes. May include
+     *                             optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $suid         Server ID of the entry.
+     *
+     * @return boolean  True on success or false on failed (item not found).
+     */
+    function deleteEntry_backend($user, $databaseURI, $suid)
+    {
+        $database = $this->_normalize($databaseURI);
+
+        $r = $this->_db->queryOne(
+            'DELETE FROM syncml_data '
+            . 'WHERE syncml_db = '
+            . $this->_db->quote($database, 'text')
+            . ' AND syncml_uid = '
+            . $this->_db->quote($user, 'text')
+            . ' AND syncml_id = '
+            . $this->_db->quote($suid, 'text'));
+        if ($this->_checkForError($r)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    function _cleanUser($user)
+    {
+        $r = $this->_db->exec('DELETE FROM syncml_data WHERE syncml_uid = '
+                              . $this->_db->quote($user, 'text'));
+        $this->_checkForError($r);
+
+        $r = $this->_db->exec('DELETE FROM syncml_map WHERE syncml_uid = '
+                              . $this->_db->quote($user, 'text'));
+        $this->_checkForError($r);
+
+        $r = $this->_db->exec('DELETE FROM syncml_anchors WHERE syncml_uid = '
+                              . $this->_db->quote($user, 'text'));
+        $this->_checkForError($r);
+
+        $r = $this->_db->exec('DELETE FROM syncml_uids WHERE syncml_uid = '
+                              . $this->_db->quote($user, 'text'));
+        $this->_checkForError($r);
+
+        $r = $this->_db->exec('DELETE FROM syncml_suidlist WHERE syncml_uid = '
+                              . $this->_db->quote($user, 'text'));
+        $this->_checkForError($r);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command.php b/framework/SyncML/SyncML/Command.php
new file mode 100644 (file)
index 0000000..4f58fb9
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+/**
+ * The SyncML_Command class provides a base class for handling all <SyncBody>
+ * commands.
+ *
+ * A SyncML command is a protocol primitive. Each SyncML command specifies to
+ * a recipient an individual operation that is to be performed.
+ *
+ * The SyncML_Command objects are hooked into the XML parser of the
+ * SyncML_ContentHandler class and are reponsible for parsing a single command
+ * inside the SyncBody section of a SyncML message. All actions that must be
+ * executed for a single SyncML command are handled by these objects, by means
+ * of the handleCommand() method.
+ *
+ * $Horde: framework/SyncML/SyncML/Command.php,v 1.23 2009/04/05 16:09:13 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command {
+
+    /**
+     * Name of the command, like 'Put'.
+     *
+     * Must be overwritten by a sub class.
+     *
+     * @var string
+     */
+    var $_cmdName;
+
+    /**
+     * The command ID (<CmdID>).
+     *
+     * @var integer
+     */
+    var $_cmdID;
+
+    /**
+     * Stack for holding the XML elements during creation of the object from
+     * the XML event flow.
+     *
+     * @var array
+     */
+    var $_stack = array();
+
+    /**
+     * Buffer for the parsed character data.
+     *
+     * @var string
+     */
+    var $_chars = '';
+
+    /**
+     * A SyncML_XMLOutput instance responsible for generating the output.
+     *
+     * @var SyncML_XMLOutput
+     */
+    var $_outputHandler;
+
+    /**
+     * Constructor.
+     *
+     * @param SyncML_XMLOutput $outputHandler  A SyncML_XMLOutput object.
+     */
+    function SyncML_Command(&$outputHandler)
+    {
+        $this->_outputHandler = &$outputHandler;
+    }
+
+    /**
+     * Start element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::startElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     * @param array $attrs     A hash with the element's attributes.
+     */
+    function startElement($uri, $element, $attrs)
+    {
+        $this->_stack[] = $element;
+    }
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        if (count($this->_stack) == 2 &&
+            $element == 'CmdID') {
+            $this->_cmdID = intval(trim($this->_chars));
+        }
+
+        if (strlen($this->_chars)) {
+            $this->_chars = '';
+        }
+
+        array_pop($this->_stack);
+    }
+
+    /**
+     * Character data handler for the XML parser, delegated from
+     * SyncML_ContentHandler::characters().
+     *
+     * @param string $str  The data string.
+     */
+    function characters($str)
+    {
+        $this->_chars .= $str;
+    }
+
+    /**
+     * Returns the command name this instance is reponsible for.
+     *
+     * @return string  The command name this object is handling.
+     */
+    function getCommandName()
+    {
+        return $this->_cmdName;
+    }
+
+    /**
+     * This method is supposed to implement the actual business logic of the
+     * command once the XML parsing is complete.
+     *
+     * @abstract
+     */
+    function handleCommand($debug = false)
+    {
+    }
+
+    /**
+     * Attempts to return a concrete SyncML_Command instance based on
+     * $command.
+     *
+     * @param string $command                  The type of the concrete
+     *                                         SyncML_Comment subclass to
+     *                                         return.
+     * @param SyncML_XMLOutput $outputHandler  A SyncML_XMLOutput object.
+     *
+     * @return SyncML_Command  The newly created concrete SyncML_Command
+     *                         instance, or false on error.
+     */
+    function &factory($command, &$outputHandler)
+    {
+        $command = basename($command);
+        $class = 'SyncML_Command_' . $command;
+
+        if (!class_exists($class)) {
+            include 'SyncML/Command/' . $command . '.php';
+        }
+        if (class_exists($class)) {
+            $cmd = new $class($outputHandler);
+        } else {
+            $msg = 'Class definition of ' . $class . ' not found.';
+            $GLOBALS['backend']->logMessage($msg, __FILE__, __LINE__,
+                                            PEAR_LOG_ERR);
+            require_once 'PEAR.php';
+            $cmd = PEAR::raiseError($msg);
+        }
+
+        return $cmd;
+    }
+
+}
+
+
diff --git a/framework/SyncML/SyncML/Command/Alert.php b/framework/SyncML/SyncML/Command/Alert.php
new file mode 100644 (file)
index 0000000..d818c26
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Alert class provides a SyncML implementation of the
+ * Alert command as defined in SyncML Representation Protocol, version 1.1,
+ * section 5.5.2.
+ *
+ * The Alert command is used for sending custom content information to the
+ * recipient. The command provides a mechanism for communicating content
+ * information, such as state information or notifications to an application
+ * on the recipient device.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Alert.php,v 1.58 2009/04/07 11:10:51 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Alert extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Alert';
+
+    /**
+     * The alert type. Should be one of the ALERT_* constants.
+     *
+     * @var integer
+     */
+    var $_alert;
+
+    /**
+     * Source database of the Alert command.
+     *
+     * @var string
+     */
+    var $_sourceLocURI;
+
+    /**
+     * Target database of the Alert command.
+     *
+     * @var string
+     */
+    var $_targetLocURI;
+
+    /**
+     * The current time this synchronization happens, from the <Meta><Next>
+     * element.
+     *
+     * @var string
+     */
+    var $_metaAnchorNext;
+
+    /**
+     * The last time when synchronization happened, from the <Meta><Last>
+     * element.
+     *
+     * @var integer
+     */
+    var $_metaAnchorLast;
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch (count($this->_stack)) {
+        case 2:
+            if ($element == 'Data') {
+                $this->_alert = intval(trim($this->_chars));
+            }
+            break;
+
+        case 4:
+            if ($element == 'LocURI') {
+                switch ($this->_stack[2]) {
+                case 'Source':
+                    $this->_sourceLocURI = trim($this->_chars);
+                    break;
+                case 'Target':
+                    $this->_targetLocURI = trim($this->_chars);
+                    break;
+                }
+            }
+            break;
+
+        case 5:
+            switch ($element) {
+            case 'Next':
+                $this->_metaAnchorNext = trim($this->_chars);
+                break;
+            case 'Last':
+                $this->_metaAnchorLast = trim($this->_chars);
+                break;
+            }
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+    /**
+     * Implements the actual business logic of the Alert command.
+     */
+    function handleCommand($debug = false)
+    {
+        $state = &$_SESSION['SyncML.state'];
+        // Handle unauthenticated first.
+        if (!$state->authenticated) {
+            $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                                RESPONSE_INVALID_CREDENTIALS);
+            return;
+        }
+
+        // Handle NEXT_MESSAGE Alert by doing nothing, except OK status
+        // response.  Exception for Funambol: here we produce the output only
+        // after an explicit ALERT_NEXT_MESSAGE.
+        if ($this->_alert == ALERT_NEXT_MESSAGE) {
+            $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                                RESPONSE_OK);
+            // @TODO: create a getDevice()->sentyncDataLate() method instead
+            // of this:
+            if (is_a($state->getDevice(), 'SyncML_Device_sync4j')) {
+                // Now send client changes to server. This will produce the
+                // <Sync> response.
+                $sync = &$state->getSync($this->_targetLocURI);
+                if ($sync) {
+                    $sync->createSyncOutput($this->_outputHandler);
+                }
+            }
+            return;
+        }
+
+        $database = $this->_targetLocURI;
+        if (!$GLOBALS['backend']->isValidDatabaseURI($database)) {
+            $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                                RESPONSE_NOT_FOUND);
+            return;
+        }
+
+        $clientAnchorNext = $this->_metaAnchorNext;
+
+        if (!$debug &&
+            ($this->_alert == ALERT_TWO_WAY ||
+             $this->_alert == ALERT_ONE_WAY_FROM_CLIENT ||
+             $this->_alert == ALERT_ONE_WAY_FROM_SERVER)) {
+            // Check if we have information about previous sync.
+            $r = $GLOBALS['backend']->readSyncAnchors($this->_targetLocURI);
+            if (is_array($r)) {
+                // Info about previous successful sync sessions found.
+                list($clientlast, $serverAnchorLast) = $r;
+                $GLOBALS['backend']->logMessage(
+                    'Previous sync found for database ' . $database
+                    . '; client timestamp: ' . $clientlast,
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                // Check if anchor sent from client matches our own stored
+                // data.
+                if ($clientlast == $this->_metaAnchorLast) {
+                    // Last sync anchors matche, TwoWaySync will do.
+                    $anchormatch = true;
+                    $GLOBALS['backend']->logMessage(
+                        'Anchor timestamps match, TwoWaySync possible. Syncing data since '
+                        . date('Y-m-d H:i:s', $serverAnchorLast),
+                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                } else {
+                    // Server and client have different anchors, enforce
+                    // SlowSync/RefreshSync
+                    $GLOBALS['backend']->logMessage(
+                        'Client requested sync with anchor timestamp '
+                        . $this->_metaAnchorLast
+                        . ' but server has recorded timestamp '
+                        . $clientlast . '. Enforcing SlowSync',
+                        __FILE__, __LINE__, PEAR_LOG_INFO);
+                    $anchormatch = false;
+                    $clientlast = 0;
+                }
+            } else {
+                // No info about previous sync, use SlowSync or RefreshSync.
+                $GLOBALS['backend']->logMessage(
+                    'No info about previous syncs found for device ' .
+                    $state->sourceURI . ' and database ' . $database,
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $clientlast = 0;
+                $serverAnchorLast = 0;
+                $anchormatch = false;
+            }
+        } else {
+            // SlowSync requested, no anchor check required.
+            $anchormatch = true;
+        }
+
+        // Determine sync type and status response code.
+        switch ($this->_alert) {
+        case ALERT_TWO_WAY:
+            if ($anchormatch) {
+                $synctype = ALERT_TWO_WAY;
+                $response = RESPONSE_OK;
+            } else {
+                $synctype = ALERT_SLOW_SYNC;
+                $response = RESPONSE_REFRESH_REQUIRED;
+            }
+            break;
+
+        case ALERT_SLOW_SYNC:
+            $synctype = ALERT_SLOW_SYNC;
+            $response = $anchormatch ? RESPONSE_OK : RESPONSE_REFRESH_REQUIRED;
+            break;
+
+        case ALERT_ONE_WAY_FROM_CLIENT:
+            if ($anchormatch) {
+                $synctype = ALERT_ONE_WAY_FROM_CLIENT;
+                $response = RESPONSE_OK;
+            } else {
+                $synctype = ALERT_REFRESH_FROM_CLIENT;
+                $response = RESPONSE_REFRESH_REQUIRED;
+            }
+            break;
+
+        case ALERT_REFRESH_FROM_CLIENT:
+            $synctype = ALERT_REFRESH_FROM_CLIENT;
+            $response = $anchormatch ? RESPONSE_OK : RESPONSE_REFRESH_REQUIRED;
+            break;
+
+        case ALERT_ONE_WAY_FROM_SERVER:
+            if ($anchormatch) {
+                $synctype = ALERT_ONE_WAY_FROM_SERVER;
+                $response = RESPONSE_OK;
+            } else {
+                $synctype = ALERT_REFRESH_FROM_SERVER;
+                $response = RESPONSE_REFRESH_REQUIRED;
+            }
+            break;
+
+        case ALERT_REFRESH_FROM_SERVER:
+            $synctype = ALERT_REFRESH_FROM_SERVER;
+            $response = $anchormatch ? RESPONSE_OK : RESPONSE_REFRESH_REQUIRED;
+            break;
+
+        case ALERT_RESUME:
+            // @TODO: Suspend and Resume is not supported yet
+            $synctype = ALERT_SLOW_SYNC;
+            $response = RESPONSE_REFRESH_REQUIRED;
+            break;
+
+        default:
+            $GLOBALS['backend']->logMessage(
+                'Unknown sync type ' . $this->_alert,
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            break;
+        }
+
+        // Now set interval to retrieve server changes from, defined by
+        // ServerAnchor [Last,Next]
+        if ($synctype != ALERT_TWO_WAY &&
+            $synctype != ALERT_ONE_WAY_FROM_CLIENT &&
+            $synctype != ALERT_ONE_WAY_FROM_SERVER) {
+            $serverAnchorLast = 0;
+            // Erase existing map:
+            if (!$debug &&
+                (($anchormatch &&
+                  CONFIG_DELETE_MAP_ON_REQUESTED_SLOWSYNC) ||
+                 (!$anchormatch &&
+                  CONFIG_DELETE_MAP_ON_ANCHOR_MISMATCH_SLOWSYNC))) {
+                $GLOBALS['backend']->eraseMap($this->_targetLocURI);
+            }
+        }
+        $serverAnchorNext = $debug ? time() : $GLOBALS['backend']->getCurrentTimeStamp();
+
+        // Now create the actual SyncML_Sync object, if it doesn't exist yet.
+        $sync = &$state->getSync($this->_targetLocURI);
+        if (!$sync) {
+            $GLOBALS['backend']->logMessage(
+                'Creating SyncML_Sync object for database '
+                . $this->_targetLocURI .  '; sync type ' . $synctype,
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $sync = new SyncML_Sync($synctype,
+                                    $this->_targetLocURI,
+                                    $this->_sourceLocURI,
+                                    $serverAnchorLast, $serverAnchorNext,
+                                    $clientAnchorNext);
+            $state->setSync($this->_targetLocURI, $sync);
+        }
+
+        $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                            $response,
+                                            $this->_targetLocURI,
+                                            $this->_sourceLocURI,
+                                            $this->_metaAnchorNext,
+                                            $this->_metaAnchorLast);
+
+        $this->_outputHandler->outputAlert($synctype,
+                                           $sync->getClientLocURI(),
+                                           $sync->getServerLocURI(),
+                                           $sync->getServerAnchorLast(),
+                                           $sync->getServerAnchorNext());
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Final.php b/framework/SyncML/SyncML/Command/Final.php
new file mode 100644 (file)
index 0000000..f75030c
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Final class provides a SyncML implementation of the
+ * Final command as defined in SyncML Representation Protocol, version 1.1,
+ * section 5.1.7.
+ *
+ * The Final command is an indicator that the SyncML message is the last
+ * message in the current SyncML package.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Final.php,v 1.35 2009/04/05 16:09:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Final extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Final';
+
+    /**
+     * Implements the actual business logic of the Alert command.
+     */
+    function handleCommand($debug = false)
+    {
+        $state = &$_SESSION['SyncML.state'];
+
+        // If the client hasn't sent us device info, request it now.
+        // @todo: only do this once, not in every msg if the client does not
+        // implement DevInf.
+        $di = $state->deviceInfo;
+        if (empty($di->Man)) {
+            $this->_outputHandler->outputGetDevInf();
+        }
+
+        $GLOBALS['backend']->logMessage('Received <Final> from client.',
+                                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $state->handleFinal($this->_outputHandler, $debug);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Get.php b/framework/SyncML/SyncML/Command/Get.php
new file mode 100644 (file)
index 0000000..95bc170
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Get class provides a SyncML implementation of the Get
+ * command as defined in SyncML Representation Protocol, version 1.1, section
+ * 5.5.7.
+ *
+ * The Get command is used to retrieve data from the recipient.  The
+ * SyncML_Command_Get class responds to a client Get request and returns the
+ * DevInf information for the SyncML server.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Get.php,v 1.43 2009/04/05 16:09:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <fourmont@gmx.de>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Get extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Get';
+
+    /**
+     * Implements the actual business logic of the Alert command.
+     */
+    function handleCommand($debug = false)
+    {
+        $state = &$_SESSION['SyncML.state'];
+
+        // Create status response.
+        $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                            RESPONSE_OK,
+                                            $state->getDevInfURI());
+        if (!$state->authenticated) {
+            return;
+        }
+
+        $this->_outputHandler->outputDevInf($this->_cmdID);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Map.php b/framework/SyncML/SyncML/Command/Map.php
new file mode 100644 (file)
index 0000000..9ff90d3
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Map class provides a SyncML implementation of the Map
+ * command as defined in SyncML Representation Protocol, version 1.1, section
+ * 5.5.8.
+ *
+ * The Map command is used to update identifier maps.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Map.php,v 1.26 2009/10/11 17:10:21 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Map extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Map';
+
+    /**
+     * Source database of the Map command.
+     *
+     * @var string
+     */
+    var $_sourceLocURI;
+
+    /**
+     * Target database of the Map command.
+     *
+     * @var string
+     */
+    var $_targetLocURI;
+
+    /**
+     * Recipient map item specifiers.
+     *
+     * @var array
+     */
+    var $_mapTargets = array();
+
+    /**
+     * Originator map item specifiers.
+     *
+     * @var array
+     */
+    var $_mapSources = array();
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch (count($this->_stack)) {
+        case 3:
+            if ($element == 'LocURI') {
+                if ($this->_stack[1] == 'Source') {
+                    $this->_sourceLocURI = trim($this->_chars);
+                } elseif ($this->_stack[1] == 'Target') {
+                    $this->_targetLocURI = trim($this->_chars);
+                }
+            }
+            break;
+
+        case 4:
+            if ($element == 'LocURI') {
+                if ($this->_stack[2] == 'Source') {
+                    $this->_mapSources[] = trim($this->_chars);
+                } elseif ($this->_stack[2] == 'Target') {
+                    $this->_mapTargets[] = trim($this->_chars);
+                }
+            }
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+    /**
+     * Implements the actual business logic of the Alert command.
+     *
+     * @todo No OK response on error.
+     */
+    function handleCommand($debug = false)
+    {
+        if (!$debug && $this->_mapSources) {
+            $state = &$_SESSION['SyncML.state'];
+            $sync = &$state->getSync($this->_targetLocURI);
+            if (!$state->authenticated) {
+                $GLOBALS['backend']->logMessage(
+                    'Not authenticated while processing <Map>',
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                foreach ($this->_mapSources as $key => $source) {
+                    $sync->createUidMap($this->_targetLocURI,
+                                        $source,
+                                        $this->_mapTargets[$key]);
+                }
+            }
+        }
+
+        // Create status response.
+        $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                            RESPONSE_OK,
+                                            $this->_targetLocURI,
+                                            $this->_sourceLocURI);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Put.php b/framework/SyncML/SyncML/Command/Put.php
new file mode 100644 (file)
index 0000000..7e9dc20
--- /dev/null
@@ -0,0 +1,268 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Put class provides a SyncML implementation of the Put
+ * command as defined in SyncML Representation Protocol, version 1.1, section
+ * 5.5.10.
+ *
+ * The Put command is used to transfer data items to a recipient network
+ * device or database. The SyncML_Command_Put class handles DevInf device
+ * information sent by the client.
+ *
+ * The data is stored in a SyncML_DeviceInfo object which is defined in
+ * Device.php and then stored in SyncML_Device as an attribute.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Put.php,v 1.40 2009/10/01 09:27:24 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Put extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Put';
+
+    /**
+     * The SyncML_DeviceInfo object where all parsed <DevInf> content is
+     * saved.
+     *
+     * <DevInf> specifies the type of the source synchronization device.
+     *
+     * @var SyncML_DeviceInfo
+     */
+    var $_devinf;
+
+    /**
+     * A SyncML_DataStore object where the information from the currently
+     * parsed <DataStore> section is saved.
+     *
+     * <DataStore> specifies the properties of a given local datastore.
+     *
+     * @var SyncML_DataStore
+     */
+    var $_currentDS;
+
+    /**
+     * The MIME content type as specified by the last <CTType> element like
+     * text/calendar or text/x-vcard.
+     *
+     * <CTType> specifies the type of a supported content type.
+     *
+     * @var string
+     */
+    var $_currentCTType;
+
+    /**
+     * The version of the MIME content type in $_currentCTType as specified by
+     * the last <VerCT> element like 1.0 for text/x-vcalendar or 2.1 for
+     * text/x-vcard.
+     *
+     * <VerCT> specifies the version of a supported content type.
+     *
+     * @var string
+     */
+    var $_VerCT;
+
+    /**
+     * A property name of the currently parsed content type (CTType), like
+     * 'DTSTART' for text/calendar or 'BDAY' for text/x-vcard.
+     *
+     * <PropName> specifies a supported property of a given content type.
+     *
+     * @var string
+     */
+    var $_currentPropName;
+
+    /**
+     * A property name of the currently parsed property name (PropName), like
+     * 'ROLE' for 'ATTENDEE' or 'HOME' for 'ADR'.
+     *
+     * <ParamName> specifies supported parameters of a given content type
+     * property.
+     *
+     * @var string
+     */
+    var $_currentParamName;
+
+    /**
+     * The name of the currently parsed DevInf extension (<Ext>) as specified
+     * by the XNam element.
+     *
+     * <XNam> specifies the name of one of the DevInf extension element types.
+     *
+     * @var string
+     */
+    var $_currentXNam;
+
+    /**
+     * Start element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::startElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     * @param array $attrs     A hash with the element's attributes.
+     */
+    function startElement($uri, $element, $attrs)
+    {
+        parent::startElement($uri, $element, $attrs);
+
+        switch (count($this->_stack)) {
+        case 1:
+            $this->_devinf = new SyncML_DeviceInfo();
+            break;
+
+        case 5:
+            if ($element == 'DataStore') {
+                $this->_currentDS = new SyncML_DataStore();
+            }
+            break;
+        }
+    }
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch ($element) {
+        case 'VerDTD':
+        case 'Man':
+        case 'Mod':
+        case 'OEM':
+        case 'FwV':
+        case 'SwV':
+        case 'HwV':
+        case 'DevID':
+        case 'DevTyp':
+            $this->_devinf->$element = trim($this->_chars);
+            break;
+
+        case 'UTC':
+        case 'SupportLargeObjs':
+        case 'SupportNumberOfChanges':
+            $this->_devinf->$element = true;
+            break;
+
+        case 'DataStore':
+            $this->_devinf->DataStores[] = $this->_currentDS;
+            break;
+
+        case 'CTCap':
+        case 'Ext':
+            // Automatically handled by subelements.
+            break;
+
+        case 'SourceRef':
+        case 'DisplayName':
+        case 'MaxGUIDSize':
+            $this->_currentDS->$element = trim($this->_chars);
+            break;
+
+        case 'Rx-Pref':
+        case 'Rx':
+        case 'Tx-Pref':
+        case 'Tx':
+            $property = str_replace('-', '_', $element);
+            $this->_currentDS->{$property}[$this->_currentCTType] = $this->_VerCT;
+            break;
+
+        case 'DSMem':
+            // Currently ignored, to be done.
+            break;
+
+        case 'SyncCap':
+            // Automatically handled by SyncType subelement.
+            break;
+
+        case 'CTType':
+            $this->_currentCTType = trim($this->_chars);
+            break;
+
+        case 'PropName':
+            $this->_currentPropName = trim($this->_chars);
+            // Reset param state.
+            unset($this->_currentParamName);
+            $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName] = new SyncML_Property();
+            break;
+
+        case 'ParamName':
+            $this->_currentParamName = trim($this->_chars);
+            $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName]->Params[$this->_currentParamName] = new SyncML_PropertyParameter();
+            break;
+
+        case 'ValEnum':
+            if (!empty($this->_currentParamName)) {
+                // We're in parameter mode.
+                $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName]->Params[$this->_currentParamName]->ValEnum[trim($this->_chars)] = true;
+            } else {
+                $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName]->ValEnum[trim($this->_chars)] = true;
+            }
+            break;
+
+        case 'DataType':
+        case 'Size':
+        case 'DisplayName':
+            if (!empty($this->_currentParamName)) {
+                // We're in parameter mode.
+                $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName]->Params[$this->_currentParamName]->{'_' . $element} = trim($this->_chars);
+            } else {
+                $this->_devinf->CTCaps[$this->_currentCTType][$this->_currentPropName]->{'_' . $element} = trim($this->_chars);
+            }
+            break;
+
+        case 'XNam':
+            $this->_currentXNam = trim($this->_chars);
+            break;
+        case 'XVal':
+            $this->_devinf->Exts[$this->_currentXNam][] = trim($this->_chars);
+            break;
+
+        case 'VerCT':
+            $this->_VerCT = trim($this->_chars);
+            break;
+        case 'SyncType':
+            $this->_currentDS->SyncCap[trim($this->_chars)] = true;
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+    /**
+     * Implements the actual business logic of the Alert command.
+     */
+    function handleCommand($debug = false)
+    {
+        $state = &$_SESSION['SyncML.state'];
+
+        // Store received data.
+        $state->deviceInfo = $this->_devinf;
+
+        // Log DevInf object.
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_DEVINF,
+                                     var_export($this->_devinf, true));
+
+        // Create status response.
+        $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                            RESPONSE_OK, '',
+                                            $state->getDevInfURI());
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Replace.php b/framework/SyncML/SyncML/Command/Replace.php
new file mode 100644 (file)
index 0000000..4d81caf
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Replace class provides a SyncML implementation of the
+ * Replace command as defined in SyncML Representation Protocol, version 1.1,
+ * section 5.5.11.
+ *
+ * The Replace command is used to replace data on the recipient device.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Replace.php,v 1.19 2009/01/06 17:49:50 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Replace extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Replace';
+
+}
diff --git a/framework/SyncML/SyncML/Command/Results.php b/framework/SyncML/SyncML/Command/Results.php
new file mode 100644 (file)
index 0000000..67f00d0
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+require_once 'SyncML/Command/Put.php';
+
+/**
+ * The SyncML_Command_Results class provides a SyncML implementation of the
+ * Results command as defined in SyncML Representation Protocol, version 1.1,
+ * section 5.5.12.
+ *
+ * The Results command is used to return the results of a Search or Get
+ * command. Currently SyncML_Command_Results behaves the same as
+ * SyncML_Command_Put. The only results we get is the same DevInf as for the
+ * Put command.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Results.php,v 1.28 2009/01/06 17:49:50 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Nathan P Sharp
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Results extends SyncML_Command_Put {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Results';
+
+}
diff --git a/framework/SyncML/SyncML/Command/Status.php b/framework/SyncML/SyncML/Command/Status.php
new file mode 100644 (file)
index 0000000..dc9c6de
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_Status class provides a SyncML implementation of the
+ * Status response as defined in SyncML Representation Protocol, version 1.1,
+ * section 5.4.
+ *
+ * This is not strictly a command but specifies the request status code for a
+ * corresponding SyncML command.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Status.php,v 1.33 2009/08/26 15:24:13 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <fourmont@gmx.de>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Status extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Status';
+
+    /**
+     * The command ID (CmdID) of the command sent to the client, that this
+     * Status response refers to.
+     *
+     * @var integer
+     */
+    var $_CmdRef;
+
+    /**
+     * The message ID (Msg) of the message sent to the client, that this Status
+     * response refers to.
+     *
+     * @var integer
+     */
+    var $_MsgRef;
+
+    /**
+     * The status response code, one of the RESPONSE_* constants.
+     *
+     * @var integer
+     */
+    var $_Status;
+
+    /**
+     * The command (Add, Replace, etc) sent to the client, that this Status
+     * response refers to.
+     *
+     * @var string
+     */
+    var $_Cmd;
+
+    /**
+     * The client ID of the sent object, that this Status response refers to.
+     *
+     * This element is optional. If specified, Status response refers to a
+     * single Item in the command sent to the client. It refers to all Items in
+     * the sent command otherwise.
+     *
+     * @var string
+     */
+    var $_TargetRef;
+
+    /**
+     * The server ID of the sent object, that this Status response refers to.
+     *
+     * This element is optional. If specified, Status response refers to a
+     * single Item in the command sent to the client. It refers to all Items in
+     * the sent command otherwise.
+     *
+     * @var string
+     */
+    var $_SourceRef;
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch (count($this->_stack)) {
+        case 2:
+            switch($element) {
+            case 'CmdRef':
+            case 'MsgRef':
+            case 'Status':
+                $this->{'_' . $element} = intval(trim($this->_chars));
+                break;
+
+            case 'Cmd':
+            case 'TargetRef':
+            case 'SourceRef':
+                $this->{'_' . $element} = trim($this->_chars);
+                break;
+            }
+            break;
+
+        case 1:
+            $state = &$_SESSION['SyncML.state'];
+            switch ($this->_Cmd) {
+            case 'Replace':
+            case 'Add':
+            case 'Delete':
+                $changes = $state->serverChanges[$this->_MsgRef];
+                /* Run through all stored changes and check if we find one
+                 * that matches this Status' message and command IDs. */
+                foreach ($changes as $db => $commands) {
+                    foreach ($commands as $cmdId => $ids) {
+                        if ($cmdId != $this->_CmdRef) {
+                            continue;
+                        }
+                        foreach ($ids as $key => $id) {
+                            /* If the Status has a SourceRef and/or TargetRef,
+                             * it's a response to a single Item only. */
+                            if ((isset($this->_SourceRef) &&
+                                 $this->_SourceRef != $id[0]) ||
+                                (isset($this->_TargetRef) &&
+                                 $this->_TargetRef != $id[1])) {
+                                continue;
+                            }
+                            /* Match found, remove from stored changes. */
+                            unset($state->serverChanges[$this->_MsgRef][$db][$this->_CmdRef][$key]);
+                            $sync = &$state->getSync($db);
+                            /* This was a Replace originally, but the object
+                             * wasn't found on the client. Try an Add
+                             * instead. */
+                            if ($this->_Cmd == 'Replace' &&
+                                $this->_Status == RESPONSE_NOT_FOUND) {
+                                $sync->setServerChange('add', $id[0], $id[1]);
+                            }
+                            if (isset($this->_SourceRef) || isset($this->_TargetRef)) {
+                                break 3;
+                            }
+                        }
+                    }
+                }
+                break;
+            }
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/Sync.php b/framework/SyncML/SyncML/Command/Sync.php
new file mode 100644 (file)
index 0000000..badad46
--- /dev/null
@@ -0,0 +1,348 @@
+<?php
+
+require_once 'SyncML/Command.php';
+require_once 'SyncML/Command/SyncElement.php';
+
+/**
+ * The SyncML_Command_Sync class provides a SyncML implementation of the Sync
+ * command as defined in SyncML Representation Protocol, version 1.1, section
+ * 5.5.15.
+ *
+ * The Sync command is used to indicate a data synchronization operation. The
+ * command handler for the Sync command is the central class to dispatch sync
+ * messages.
+ *
+ * During parsing of the received XML, the actual sync commands (Add, Replace,
+ * Delete) from the client are stored in the $_syncElements attribute.  When
+ * the output method of SyncML_Command_Sync is called, these elements are
+ * processed and the resulting status messages created.
+ * Then the server modifications are sent back to the client by the
+ * handleSync() method which is called from within the output method.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/Sync.php,v 1.66 2009/10/02 22:37:57 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_Command_Sync extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'Sync';
+
+    /**
+     * Source database of the <Sync> command.
+     *
+     * @var string
+     */
+    var $_sourceURI;
+
+    /**
+     * Target database of the <Sync> command.
+     *
+     * @var string
+     */
+    var $_targetURI;
+
+    /**
+     * SyncML_SyncElement object for the currently parsed sync command.
+     *
+     * @var SyncML_SyncElement
+     */
+    var $_curItem;
+
+    /**
+     * List of all SyncML_SyncElement objects that have parsed.
+     *
+     * @var array
+     */
+    var $_syncElements = array();
+
+    /**
+     * The MIME content type of the currently parsed sync command as specified
+     * by the <Type> element inside a <Meta> section.
+     *
+     * @var string
+     */
+    var $_contentType = 'text/plain';
+
+    /**
+     * Encoding format of the content as specified in the <Meta><Format>
+     * element, like 'b64'.
+     *
+     * @var string
+     */
+    var $_contentFormat = 'chr';
+
+    /**
+     * The command ID (<CmdID>) of the currently parsed sync command.
+     *
+     * This is different from the command ID of the <Sync> command itself.
+     *
+     * @var integer
+     */
+    var $_itemCmdID;
+
+    /**
+     * Name of the currently parsed sync command, like 'Add'.
+     *
+     * @var string
+     */
+    var $_elementType;
+
+    /**
+     * Whether a <MoreData> element has indicated that the sync command is
+     * split into several SyncML message chunks.
+     *
+     * @var boolean
+     */
+    var $_itemMoreData;
+
+    /**
+     * The size of the data item of the currently parsed sync command in bytes
+     * as specified by a <Size> element.
+     *
+     * @var integer
+     */
+    var $_itemSize;
+
+    /**
+     * Start element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::startElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     * @param array $attrs     A hash with the element's attributes.
+     */
+    function startElement($uri, $element, $attrs)
+    {
+        parent::startElement($uri, $element, $attrs);
+        $state = &$_SESSION['SyncML.state'];
+
+        switch (count($this->_stack)) {
+        case 2:
+            if ($element == 'Replace' ||
+                $element == 'Add' ||
+                $element == 'Delete') {
+                $this->_contentType = 'text/plain';
+                $this->_elementType = $element;
+                $this->_itemSize = null;
+            }
+            break;
+
+        case 3:
+            if ($element == 'Item') {
+                if (isset($state->curSyncItem)) {
+                    // Copy from state in case of <MoreData>.
+                    $this->_curItem = $state->curSyncItem;
+                    // Set CmdID to the current CmdId, not the initial one
+                    // from the first message.
+                    $this->_curItem->cmdID = $this->_itemCmdID;
+                    unset($state->curSyncItem);
+                } else {
+                    $this->_curItem = new SyncML_SyncElement(
+                        $state->getSync($this->_targetURI),
+                        $this->_elementType,
+                        $this->_itemCmdID,
+                        $this->_itemSize);
+                }
+                $this->_itemMoreData = false;
+            }
+        }
+    }
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch (count($this->_stack)) {
+        case 3:
+            switch ($element) {
+            case 'LocURI':
+                if (!isset($this->_currentSyncElement)) {
+                    if ($this->_stack[1] == 'Source') {
+                        $this->_sourceURI = trim($this->_chars);
+                    } elseif ($this->_stack[1] == 'Target') {
+                        $this->_targetURI = trim($this->_chars);
+                    }
+                }
+                break;
+
+            case 'Item':
+                if ($this->_itemMoreData) {
+                    // Store to continue in next session.
+                    $_SESSION['SyncML.state']->curSyncItem = $this->_curItem;
+                } else {
+                    // Finished. Store to syncElements[].
+                    if (empty($this->_curItem->contentType)) {
+                        $this->_curItem->contentType = $this->_contentType;
+                    }
+                    if (empty($this->_curItem->contentFormat)) {
+                        $this->_curItem->contentFormat = $this->_contentFormat;
+                    }
+
+                    $this->_syncElements[] = $this->_curItem;
+                    // @todo: check if size matches strlen(content) when
+                    // size>0, esp. in case of <MoreData>.
+                    unset($this->_curItem);
+                }
+                break;
+
+            case 'CmdID':
+                $this->_itemCmdID = trim($this->_chars);
+                break;
+            }
+            break;
+
+        case 4:
+            switch ($element) {
+            case 'Format':
+                if ($this->_stack[2] == 'Meta') {
+                    $this->_contentFormat = trim($this->_chars);
+                }
+                break;
+            case 'Type':
+                if ($this->_stack[2] == 'Meta') {
+                    $this->_contentType = trim($this->_chars);
+                }
+                break;
+            case 'Data':
+                // Don't trim, because we have to check the raw content's size.
+                $this->_curItem->content .= $this->_chars;
+                break;
+            case 'MoreData':
+                $this->_itemMoreData = true;
+                break;
+            case 'Size':
+                $this->_itemSize = $this->_chars;
+                break;
+            }
+            break;
+
+        case 5:
+            switch ($element) {
+            case 'LocURI':
+                if ($this->_stack[3] == 'Source') {
+                    $this->_curItem->cuid = trim($this->_chars);
+                } elseif ($this->_stack[3] == 'Target') {
+                    // Not used: we ignore "suid proposals" from client.
+                }
+                break;
+
+            case 'Format':
+                if ($this->_stack[3] == 'Meta') {
+                    $this->_curItem->contentFormat = trim($this->_chars);
+                }
+                break;
+
+            case 'Type':
+                $this->_curItem->contentType = trim($this->_chars);
+                break;
+            }
+            break;
+
+        case 6:
+            if ($element == 'Type') {
+                $this->_curItem->contentType = trim($this->_chars);
+            }
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+    /**
+     * Implements the actual business logic of the Sync command.
+     */
+    function handleCommand($debug = false)
+    {
+        $state = &$_SESSION['SyncML.state'];
+
+        // Handle unauthenticated first.
+        if (!$state->authenticated) {
+            $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                                RESPONSE_INVALID_CREDENTIALS);
+            return;
+        }
+
+        if ($debug) {
+            $sync = &$state->getSync($this->_targetURI);
+            $sync = new SyncML_Sync(ALERT_TWO_WAY,
+                                    $this->_targetURI,
+                                    $this->_sourceURI,
+                                    0, 0, 0);
+        } else {
+            $sync = &$state->getSync($this->_targetURI);
+            $sync->addSyncReceived();
+
+            if (!is_object($sync)) {
+                $GLOBALS['backend']->logMessage(
+                    'No sync object found for URI ' . $this->_targetURI,
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+                // @todo: create meaningful status code here.
+            }
+        }
+
+        /* @todo: Check: do we send a status for every sync or only once after
+         * one sync is completed?
+         * SE K750 expects Status response to be sent before Sync output
+         * by server is produced. */
+        $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
+                                            RESPONSE_OK,
+                                            $this->_targetURI,
+                                            $this->_sourceURI);
+
+        // Here's where client modifications are processed.
+        $device = $state->getDevice();
+        $omit = $device->omitIndividualSyncStatus();
+        foreach ($this->_syncElements as $item) {
+            $result = $sync->handleClientSyncItem($this->_outputHandler, $item);
+            if (!$omit) {
+                $this->_outputStatus($item);
+            }
+        }
+
+        if ($this->_itemMoreData) {
+            // Last item had <MoreData> element, produce appropriate response.
+            $this->_outputHandler->outputStatus(
+                $state->curSyncItem->cmdID,
+                $state->curSyncItem->elementType,
+                RESPONSE_CHUNKED_ITEM_ACCEPTED_AND_BUFFERED,
+                '',
+                $state->curSyncItem->cuid);
+            // @todo: check if we have to send Alert NEXT_MESSAGE here!
+        }
+    }
+
+    /**
+     * Creates the <Status> response for one Add|Replace|Delete SyncElement.
+     *
+     * @param SyncML_SyncElement $element  The element for which the status is
+     *                                     to be created.
+     */
+    function _outputStatus($element)
+    {
+        // @todo: produce valid status
+        $this->_outputHandler->outputStatus($element->cmdID,
+                                            $element->elementType,
+                                            $element->responseCode,
+                                            '',
+                                            $element->cuid);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Command/SyncElement.php b/framework/SyncML/SyncML/Command/SyncElement.php
new file mode 100644 (file)
index 0000000..88b5bfa
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * The class SyncML_Command_SyncElement stores information from the <Add>,
+ * <Delete> and <Replace> elements found inside a <Sync> command.
+ *
+ * Instances of this class are created during the XML parsing by
+ * SyncML_Command_Sync.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/SyncElement.php,v 1.18 2009/04/05 20:12:12 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0.5
+ * @package SyncML
+ */
+class SyncML_SyncElement {
+
+    /**
+     * The MIME content type of the sync command.
+     *
+     * @var string
+     */
+    var $contentType;
+
+    /**
+     * Encoding format of the content as specified in the <Meta><Format>
+     * element, like 'b64'.
+     *
+     * @var string
+     */
+    var $contentFormat;
+
+    /**
+     * The actual data content of the sync command.
+     *
+     * @var string $content
+     */
+    var $content = '';
+
+    /**
+     * The size of the data item of the sync command in bytes as specified by
+     * a <Size> element.
+     *
+     * @var integer
+     */
+    var $size;
+
+    /**
+     * The command ID (<CmdID>) of the sync command.
+     *
+     * @var integer
+     */
+    var $cmdID;
+
+    /**
+     * Name of the sync command, like 'Add'.
+     *
+     * @var string
+     */
+    var $elementType;
+
+    /**
+     * The client ID for the data item processed in the sync command.
+     *
+     * @var string
+     */
+    var $cuid;
+
+    /**
+     * The code to be sent as status response in a <Status> element, one of
+     * the RESPONSE_* constants.
+     *
+     * This is set in SyncML_Sync::handleClientSyncItem() when "processing"
+     * the item.
+     *
+     * @var integer
+     */
+    var $responseCode;
+
+    /**
+     * The Sync object for this element is part of.
+     *
+     * @var object SyncML_Sync
+     */
+    var $sync;
+
+    /**
+     * Constructor.
+     *
+     * @param SyncML_Sync $sync
+     * @param string $elementType
+     * @param integer $cmdID
+     * @param integer $size
+     */
+    function SyncML_SyncElement(&$sync, $elementType, $cmdID, $size)
+    {
+        $this->sync = &$sync;
+        $this->elementType = $elementType;
+        $this->cmdID = $cmdID;
+        $this->size = $size;
+    }
+
+}
+
diff --git a/framework/SyncML/SyncML/Command/SyncHdr.php b/framework/SyncML/SyncML/Command/SyncHdr.php
new file mode 100644 (file)
index 0000000..970b010
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+
+require_once 'SyncML/Command.php';
+
+/**
+ * The SyncML_Command_SyncHdr class provides a SyncML implementation of the
+ * SyncHdr as defined in SyncML Representation Protocol, version 1.1, section
+ * 5.2.2.
+ *
+ * SyncHdr is not really a sync command, but this class takes advantage of the
+ * XML parser in SyncML_Command.
+ *
+ * $Horde: framework/SyncML/SyncML/Command/SyncHdr.php,v 1.10 2009/04/05 21:31:57 jan Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.2
+ * @package SyncML
+ */
+class SyncML_Command_SyncHdr extends SyncML_Command {
+
+    /**
+     * Name of the command.
+     *
+     * @var string
+     */
+    var $_cmdName = 'SyncHdr';
+
+    /**
+     * Username as specified in the <LocName> element.
+     *
+     * @var string
+     */
+    var $user;
+
+    /**
+     * Id of this SyncML session as specified in the <SessionID> element.
+     *
+     * This is not to confuse with the PHP session id, though it is part of
+     * the generated PHP session id.
+     *
+     * @var string
+     */
+    var $_sessionID;
+
+    /**
+     * SyncML protocol version as specified in the <VerProto> element.
+     *
+     * 0 for SyncML 1.0, 1 for SyncML 1.1, etc.
+     *
+     * @var integer
+     */
+    var $_version;
+
+    /**
+     * Id of the current message as specified in the <MsgID> element.
+     *
+     * @var integer
+     */
+    var $_message;
+
+    /**
+     * The target URI as specified by the <Target><LocURI> element.
+     *
+     * This is normally the URL of the Horde RPC server. However the client is
+     * free to send anything.
+     *
+     * @var string
+     */
+    var $_targetURI;
+
+    /**
+     * The source URI as specified by the <Source><LocURI> element.
+     *
+     * @var string
+     */
+    var $_sourceURI;
+
+    /**
+     * Authentication credential as specified by the <Cred><Data> element.
+     *
+     * @var string
+     */
+    var $credData;
+
+    /**
+     * Encoding format of $credData as specified in the <Cred><Meta><Format>
+     * element like 'b64'.
+     *
+     * @var string
+     */
+    var $credFormat;
+
+    /**
+     * Media type of $credData as specified in the <Cred><Meta><Type> element
+     * like 'auth-basic'.
+     *
+     * @var string
+     */
+    var $credType;
+
+    /**
+     * Maximum size of a SyncML message in bytes as specified by the
+     * <Meta><MaxMsgSize> element.
+     *
+     * @var integer
+     */
+    var $_maxMsgSize;
+
+    /**
+     * End element handler for the XML parser, delegated from
+     * SyncML_ContentHandler::endElement().
+     *
+     * @param string $uri      The namespace URI of the element.
+     * @param string $element  The element tag name.
+     */
+    function endElement($uri, $element)
+    {
+        switch (count($this->_stack)) {
+        case 2:
+            if ($element == 'VerProto') {
+                // </VerProto></SyncHdr></SyncML>
+                if (trim($this->_chars) == 'SyncML/1.1') {
+                    $this->_version = 1;
+                } elseif (trim($this->_chars) == 'SyncML/1.2') {
+                    $this->_version = 2;
+                } else {
+                    $this->_version = 0;
+                }
+            } elseif ($element == 'SessionID') {
+                // </SessionID></SyncHdr></SyncML>
+                $this->_sessionID = trim($this->_chars);
+            } elseif ($element == 'MsgID') {
+                // </MsgID></SyncHdr></SyncML>
+                $this->_message = intval(trim($this->_chars));
+            }
+            break;
+
+        case 3:
+            if ($element == 'LocURI') {
+                if ($this->_stack[1] == 'Source') {
+                    // </LocURI></Source></SyncHdr></SyncML>
+                    $this->_sourceURI = trim($this->_chars);
+                } elseif ($this->_stack[1] == 'Target') {
+                    // </LocURI></Target></SyncHdr></SyncML>
+                    $this->_targetURI = trim($this->_chars);
+                }
+            } elseif ($element == 'LocName') {
+                if ($this->_stack[1] == 'Source') {
+                    // </LocName></Source></SyncHdr></SyncML>
+                    $this->user = trim($this->_chars);
+                }
+            } elseif ($element == 'Data') {
+                    // </Data></Cred></SyncHdr></SyncML>
+                if ($this->_stack[1] == 'Cred') {
+                    $this->credData = trim($this->_chars);
+                }
+            } elseif ($element == 'MaxMsgSize') {
+                // </MaxMsgSize></Meta></SyncHdr></SyncML>
+                $this->_maxMsgSize = intval($this->_chars);
+            }
+            break;
+
+        case 4:
+            if ($this->_stack[1] == 'Cred') {
+                if ($element == 'Format') {
+                    // </Format></Meta></Cred></SyncHdr></SyncML>
+                    $this->credFormat = trim($this->_chars);
+                } elseif ($element == 'Type') {
+                    // </Type></Meta></Cred></SyncHdr></SyncML>
+                    $this->credType = trim($this->_chars);
+                }
+            }
+            break;
+        }
+
+        parent::endElement($uri, $element);
+    }
+
+    /**
+     * Starts the PHP session and instantiates the global SyncML_State object
+     * if doesn't exist yet.
+     */
+    function setupState()
+    {
+        $GLOBALS['backend']->sessionStart($this->_sourceURI,
+                                          $this->_sessionID);
+
+        if (!isset($_SESSION['SyncML.state'])) {
+            $GLOBALS['backend']->logMessage(
+                'New session created: ' . session_id(),
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $_SESSION['SyncML.state'] = new SyncML_State($this->_sourceURI,
+                                                         $this->user,
+                                                         $this->_sessionID);
+        } else {
+            $GLOBALS['backend']->logMessage(
+                'Existing session continued: ' . session_id(),
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+
+        $state = &$_SESSION['SyncML.state'];
+        $state->setVersion($this->_version);
+        $state->messageID = $this->_message;
+        $state->targetURI = $this->_targetURI;
+        $state->sourceURI = $this->_sourceURI;
+        $state->sessionID = $this->_sessionID;
+        if (!empty($this->_maxMsgSize)) {
+            $state->maxMsgSize = $this->_maxMsgSize;
+        }
+
+        $GLOBALS['backend']->setupState($state);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Constants.php b/framework/SyncML/SyncML/Constants.php
new file mode 100644 (file)
index 0000000..6a75b37
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+/**
+ * $Horde: framework/SyncML/SyncML/Constants.php,v 1.20 2009/01/06 17:49:48 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+
+define('ALERT_DISPLAY', 100);
+
+define('ALERT_TWO_WAY', 200);
+define('ALERT_SLOW_SYNC', 201);
+define('ALERT_ONE_WAY_FROM_CLIENT', 202);
+define('ALERT_REFRESH_FROM_CLIENT', 203);
+define('ALERT_ONE_WAY_FROM_SERVER', 204);
+define('ALERT_REFRESH_FROM_SERVER', 205);
+
+// Not implemented.
+define('ALERT_TWO_WAY_BY_SERVER', 206);
+define('ALERT_ONE_WAY_FROM_CLIENT_BY_SERVER', 207);
+define('ALERT_REFRESH_FROM_CLIENT_BY_SERVER', 208);
+define('ALERT_ONE_WAY_FROM_SERVER_BY_SERVER', 209);
+define('ALERT_REFRESH_FROM_SERVER_BY_SERVER', 210);
+
+define('ALERT_RESULT_ALERT',   221);
+define('ALERT_NEXT_MESSAGE',   222);
+define('ALERT_NO_END_OF_DATA', 223);
+
+// Not (really) implemented.
+define('ALERT_SUSPEND',        224); // New in SyncML 1.2
+define('ALERT_RESUME',         225); // New in SyncML 1.2
+
+define('MIME_SYNCML_XML', 'application/vnd.syncml+xml');
+define('MIME_SYNCML_WBXML', 'application/vnd.syncml+wbxml');
+
+define('MIME_SYNCML_DEVICE_INFO_XML', 'application/vnd.syncml-devinf+xml');
+define('MIME_SYNCML_DEVICE_INFO_WBXML', 'application/vnd.syncml-devinf+wbxml');
+
+define('MIME_TEXT_PLAIN', 'text/plain');
+define('MIME_VCARD_V21', 'text/x-vcard');
+define('MIME_VCARD_V30', 'text/vcard');
+
+define('MIME_VCALENDAR', 'text/x-vcalendar');
+define('MIME_ICALENDAR', 'text/calendar');
+define('MIME_XML_ICALENDAR', 'application/vnd.syncml-xcal');
+
+define('MIME_MESSAGE', 'text/message');
+
+define('MIME_SYNCML_XML_EMAIL', 'application/vnd.syncml-xmsg');
+define('MIME_SYNCML_XML_BOOKMARK', 'application/vnd.syncml-xbookmark');
+define('MIME_SYNCML_RELATIONAL_OBJECT', 'application/vnd.syncml-xrelational');
+
+define('RESPONSE_IN_PROGRESS', 101);
+
+define('RESPONSE_OK', 200);
+define('RESPONSE_ITEM_ADDED', 201);
+define('RESPONSE_ACCEPTED_FOR_PROCESSING', 202);
+define('RESPONSE_NONAUTHORIATATIVE_RESPONSE', 203);
+define('RESPONSE_NO_CONTENT', 204);
+define('RESPONSE_RESET_CONTENT', 205);
+define('RESPONSE_PARTIAL_CONTENT', 206);
+define('RESPONSE_CONFLICT_RESOLVED_WITH_MERGE', 207);
+define('RESPONSE_CONFLICT_RESOLVED_WITH_CLIENT_WINNING', 208);
+define('RESPONSE_CONFILCT_RESOLVED_WITH_DUPLICATE', 209);
+define('RESPONSE_DELETE_WITHOUT_ARCHIVE', 210);
+define('RESPONSE_ITEM_NO_DELETED', 211);
+define('RESPONSE_AUTHENTICATION_ACCEPTED', 212);
+define('RESPONSE_CHUNKED_ITEM_ACCEPTED_AND_BUFFERED', 213);
+define('RESPONSE_OPERATION_CANCELLED', 214);
+define('RESPONSE_NO_EXECUTED', 215);
+define('RESPONSE_ATOMIC_ROLL_BACK_OK', 216);
+
+define('RESPONSE_MULTIPLE_CHOICES', 300);
+// Need to change names.
+// define('RESPONSE_MULTIPLE_CHOICES', 301);
+// define('RESPONSE_MULTIPLE_CHOICES', 302);
+// define('RESPONSE_MULTIPLE_CHOICES', 303);
+// define('RESPONSE_MULTIPLE_CHOICES', 304);
+define('RESPONSE_USE_PROXY', 305);
+
+define('RESPONSE_BAD_REQUEST', 400);
+define('RESPONSE_INVALID_CREDENTIALS', 401);
+// Need to change names.
+// define('RESPONSE_INVALID_CREDENTIALS', 402);
+// define('RESPONSE_INVALID_CREDENTIALS', 403);
+define('RESPONSE_NOT_FOUND', 404);
+// Need to change names.
+// define('RESPONSE_INVALID_CREDENTIALS', 405);
+// define('RESPONSE_INVALID_CREDENTIALS', 406);
+define('RESPONSE_CREDENTIALS_MISSING', 407);
+// define('RESPONSE_INVALID_CREDENTIALS', 408);
+// define('RESPONSE_INVALID_CREDENTIALS', 409);
+// define('RESPONSE_INVALID_CREDENTIALS', 410);
+// define('RESPONSE_INVALID_CREDENTIALS', 411);
+// define('RESPONSE_INVALID_CREDENTIALS', 412);
+// define('RESPONSE_INVALID_CREDENTIALS', 413);
+// define('RESPONSE_INVALID_CREDENTIALS', 414);
+// define('RESPONSE_INVALID_CREDENTIALS', 415);
+define('RESPONSE_REQUEST_SIZE_TOO_BIG', 416);
+// Need to change names.
+// define('RESPONSE_INVALID_CREDENTIALS', 417);
+// define('RESPONSE_INVALID_CREDENTIALS', 418);
+// define('RESPONSE_INVALID_CREDENTIALS', 419);
+// define('RESPONSE_INVALID_CREDENTIALS', 420);
+// define('RESPONSE_INVALID_CREDENTIALS', 421);
+// define('RESPONSE_INVALID_CREDENTIALS', 422);
+// define('RESPONSE_INVALID_CREDENTIALS', 423);
+define('RESPONSE_SIZE_MISMATCH', 424);
+
+define('RESPONSE_COMMAND_FAILED', 500);
+// Need to change names.
+// define('RESPONSE_COMMAND_FAILED', 501);
+// define('RESPONSE_COMMAND_FAILED', 502);
+// define('RESPONSE_COMMAND_FAILED', 503);
+// define('RESPONSE_COMMAND_FAILED', 504);
+// define('RESPONSE_COMMAND_FAILED', 505);
+// define('RESPONSE_COMMAND_FAILED', 506);
+// define('RESPONSE_COMMAND_FAILED', 507);
+define('RESPONSE_REFRESH_REQUIRED', 508);
+// define('RESPONSE_COMMAND_FAILED', 509);
+// define('RESPONSE_COMMAND_FAILED', 510);
+// define('RESPONSE_COMMAND_FAILED', 511);
+// define('RESPONSE_COMMAND_FAILED', 512);
+// define('RESPONSE_COMMAND_FAILED', 513);
+// define('RESPONSE_COMMAND_FAILED', 514);
+// define('RESPONSE_COMMAND_FAILED', 515);
+// define('RESPONSE_ATOMIC_ROLL_BACK_FAILED', 516);
+
+define('NAME_SPACE_URI_SYNCML', 'syncml:syncml');
+define('NAME_SPACE_URI_SYNCML_1_1', 'syncml:syncml1.1');
+define('NAME_SPACE_URI_SYNCML_1_2', 'syncml:syncml1.2');
+define('NAME_SPACE_URI_METINF', 'syncml:metinf');
+define('NAME_SPACE_URI_METINF_1_1', 'syncml:metinf');
+define('NAME_SPACE_URI_METINF_1_2', 'syncml:metinf');
+define('NAME_SPACE_URI_DEVINF', 'syncml:devinf');
+define('NAME_SPACE_URI_DEVINF_1_1', 'syncml:devinf');
+define('NAME_SPACE_URI_DEVINF_1_2', 'syncml:devinf');
+
+/**
+ * Maximum Size of a data object. Currently global for all databases.
+ */
+define('SERVER_MAXOBJSIZE', 1000000000);
+
+/**
+ * Maximum size for one sync message as defined by SyncML protocol spec.
+ */
+define('SERVER_MAXMSGSIZE', 1000000000);
+
+/**
+ * The "safety margin" for the closing tags when finishing a message.
+ *
+ * When exporting a data entry, we have to ensure that the size of the
+ * complete message does not exceed MaxMsgSize sent by the client.
+ */
+define('MSG_TRAILER_LEN', 150);
+
+/**
+ * Standard size for a complete but empty SyncML message. Used in estimating
+ * the size for a message.
+ */
+define('MSG_DEFAULT_LEN', 1000);
+
+/**
+ * If true the client uid<->server uid map will be deleted when a SlowSync is
+ * requested.
+ *
+ * This produces duplicates if there are entries in the client and the server.
+ * This need to be true for the test conformance suite.
+ */
+define('CONFIG_DELETE_MAP_ON_REQUESTED_SLOWSYNC', true);
+
+/**
+ * If true the client uid<->server uid map will be deleted when a SlowSync is
+ * done due to an anchor mismatch. An anchor mismatch may happen if a session
+ * terminates unexpectedly.
+ */
+define('CONFIG_DELETE_MAP_ON_ANCHOR_MISMATCH_SLOWSYNC', false);
diff --git a/framework/SyncML/SyncML/Device.php b/framework/SyncML/SyncML/Device.php
new file mode 100644 (file)
index 0000000..8800002
--- /dev/null
@@ -0,0 +1,287 @@
+<?php
+/**
+ * The SyncML_Device:: class provides functionality that is potentially
+ * (client) device dependant.
+ *
+ * If a sync client needs any kind of special conversion of the data sent to
+ * it or received from it, this is done here. There are two sources of
+ * information to identify an device: The first (and better) one is the DevInf
+ * device info sent by the device during a request. If DevInf is not supported
+ * or sent by the client, the Source/LocURI of the device request might be
+ * sufficent to identify it.
+ *
+ * $Horde: framework/SyncML/SyncML/Device.php,v 1.44 2009/01/06 17:49:48 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device {
+
+    /**
+     * Attempts to return a concrete SyncML_Device instance based on $driver.
+     *
+     * @param string $driver  The type of concrete SyncML_Device subclass to
+     *                        return.
+     *
+     * @return SyncML_Device  The newly created concrete SyncML_Device
+     *                        instance, or false on error.
+     */
+    function factory($driver)
+    {
+        $driver = basename($driver);
+
+        if (empty($driver) || $driver == 'none' || $driver == 'default') {
+            $GLOBALS['backend']->logMessage(
+                'Using default device class',
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return new SyncML_Device();
+        }
+
+        $class = 'SyncML_Device_' . $driver;
+        if (!class_exists($class)) {
+            include 'SyncML/Device/' . $driver . '.php';
+        }
+        if (!class_exists($class)) {
+            return false;
+        }
+
+        $device = new $class();
+        $GLOBALS['backend']->logMessage('Created device class ' . $class,
+                                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return $device;
+    }
+
+    /**
+     * Returns the guessed content type for a database URI.
+     *
+     * When a client sends data during a sync but does not provide information
+     * about the MIME content type with this individual item, this function
+     * returns the content type the item is supposed to be in.
+     *
+     * @param string $database  A database URI.
+     *
+     * @return string  A MIME type that might match the database URI.
+     */
+    function getPreferredContentType($database)
+    {
+        $database = $GLOBALS['backend']->_normalize($database);
+
+        /* Use some wild guessings. */
+        if (strpos($database, 'contact') !== false ||
+            strpos($database, 'card') !== false) {
+            return 'text/x-vcard';
+        } elseif (strpos($database, 'note') !== false ||
+                  strpos($database, 'memo') !== false) {
+            return 'text/plain';
+        } elseif (strpos($database, 'task') !== false ||
+                  strpos($database, 'cal') !== false ||
+                  strpos($database, 'event') !== false) {
+            return 'text/calendar';
+        }
+    }
+
+    /**
+     * Returns the preferrred MIME content type of the client for the given
+     * sync data type (contacts/tasks/notes/calendar).
+     *
+     * The result is passed as an option to the backend export functions.
+     * This is not the content type ultimately passed to the client but rather
+     * the content type presented to the backend export functions.
+     *
+     * After the data is retrieved from the backend, convertServer2Client()
+     * can do some post-processing and set the correct content type acceptable
+     * for the client if necessary.
+     *
+     * The default implementation tries to extract the content type from the
+     * device info. If this does not work, some defaults are used.
+     *
+     * If the client does not provice proper DevInf data, this function may
+     * have to be overwritten to return the correct values.
+     *
+     * @param string $serverSyncURI  The URI for the server database: contacts,
+     *                               notes, calendar or tasks.
+     * @param string $sourceSyncURI  The URI for the client database. This is
+     *                               needed as the DevInf is grouped by
+     *                               sourceSyncURIs.
+     */
+    function getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI)
+    {
+        $di = $_SESSION['SyncML.state']->deviceInfo;
+        $ds = $di->getDataStore($sourceSyncURI);
+        if (!empty($ds)) {
+            $r = $ds->getPreferredRXContentType();
+            if (!empty($r)) {
+                return $r;
+            }
+        }
+
+        $database = $GLOBALS['backend']->_normalize($serverSyncURI);
+
+        /* No information in DevInf, use some wild guessings. */
+        if (strpos($database, 'contact') !== false ||
+            strpos($database, 'card') !== false) {
+            return 'text/x-vcard';
+        } elseif (strpos($database, 'note') !== false ||
+                  strpos($database, 'memo') !== false) {
+            // SyncML conformance suite expects this rather than text/x-vnote
+            return 'text/plain';
+        } elseif (strpos($database, 'task') !== false ||
+                  strpos($database, 'cal') !== false ||
+                  strpos($database, 'event') !== false) {
+            return 'text/calendar';
+        }
+    }
+
+    /**
+     * Converts the content received from the client for the backend.
+     *
+     * Currently strips UID (primary key) information as client and server
+     * might use different ones.
+     *
+     * Charset conversions might be added here too.
+     *
+     * @todo remove UID stripping or move it anywhere else.
+     *
+     * @param string $content      The content to convert.
+     * @param string $contentType  The content type of the content.
+     *
+     * @return array  Two-element array with the converted content and the
+     *                (possibly changed) new content type.
+     */
+    function convertClient2Server($content, $contentType)
+    {
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\nInput received from client ($contentType):\n$content\n");
+
+        // Always remove client UID. UID will be seperately passed in XML.
+        $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/',
+                                '\1', $content, 1);
+
+        return array($content, $contentType);
+    }
+
+    /**
+     * Converts the content from the backend to a format suitable for the
+     * client device.
+     *
+     * Strips the UID (primary key) information as client and server might use
+     * different ones.
+     *
+     * Charset conversions might be added here too.
+     *
+     * @param string $content      The content to convert
+     * @param string $contentType  The content type of content as returned
+     *                             from the backend
+     * @param string $database     The server database URI.
+     *
+     * @return array  Three-element array with the converted content, the
+     *                (possibly changed) new content type, and encoding type
+     *                (like b64 as used by Funambol).
+     */
+    function convertServer2Client($content, $contentType, $database)
+    {
+        if (is_array($contentType)) {
+            $contentType = $contentType['ContentType'];
+        }
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\nOutput received from backend ($contentType):\n" . $content
+            . "\n");
+
+        /* Always remove server UID. UID will be seperately passed in XML. */
+        $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/',
+                                '\1', $content, 1);
+
+        if ($this->useLocalTime()) {
+            $content = preg_replace_callback(
+                '/\d{8}T\d{6}Z/',
+                array($this, 'convertUTC2LocalTime'),
+                $content);
+        }
+
+        return array($content, $contentType, null);
+    }
+
+    /**
+     * Returns whether the device handles tasks and events in a single
+     * "calendar" sync.
+     *
+     * This requires special actions on our side as we store this in different
+     * backend databases.
+     *
+     * @return boolean  True if tasks and events are processed in a single
+     *                  request.
+     */
+    function handleTasksInCalendar()
+    {
+        return false;
+    }
+
+    /**
+     * Returns whether to send individual status response for each Add, Delete
+     * and Replace.
+     *
+     * @return boolean  False if individual status responses should be send.
+     */
+    function omitIndividualSyncStatus()
+    {
+        return false;
+    }
+
+    /**
+     * Returns whether the payload data should be enclosed in a [CDATA[
+     * section when sending via XML.
+     *
+     * The synchronized data may contain XML special characters like &amp;,
+     * &lt; or &gt;. Clients might choke when sending these embedded in XML.
+     * The data should be enclosed in [CDATA[ in these cases.  This applies
+     * only to XML, not to WBXML devices.
+     *
+     * @return boolean  True if the data should be enclosed in [CDATA[.
+     */
+    function useCdataTag()
+    {
+        return true;
+    }
+
+    /**
+     * Returns whether the device accepts datetimes only in local time format
+     * (DTSTART:20061222T130000) instead of the more robust UTC time
+     * (DTSTART:20061222T110000Z).
+     *
+     * @return boolean  True if the client doesn't accept UTC datetimes.
+     */
+    function useLocalTime()
+    {
+        return false;
+    }
+
+    /**
+     * Converts an UTC timestamp like "20061222T110000Z" into a local
+     * timestamp like "20061222T130000" using the server timezone.
+     *
+     * @param string $utc  A datetime string in UTC.
+     *
+     * @return string  The datetime string converted to the local timezone.
+     */
+    function convertUTC2LocalTime($utc)
+    {
+        $dateParts = explode('T', $utc[0]);
+        $date = Horde_iCalendar::_parseDate($dateParts[0]);
+        $time = Horde_iCalendar::_parseTime($dateParts[1]);
+
+        // We don't know the timezone so assume local timezone.
+        $ts = @gmmktime($time['hour'], $time['minute'], $time['second'],
+                        $date['month'], $date['mday'], $date['year']);
+
+        return date('Ymd\THis',$ts);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Device/Nokia.php b/framework/SyncML/SyncML/Device/Nokia.php
new file mode 100644 (file)
index 0000000..fb68673
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * The SyncML_Device_Nokia:: class provides functionality that is
+ * specific to the Nokia SyncML clients.
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/SyncML/SyncML/Device/Nokia.php,v 1.19 2009/08/18 17:16:43 jan Exp $
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device_Nokia extends SyncML_Device {
+
+    /**
+     * Converts the content received from the client for the backend.
+     *
+     * @param string $content      The content to convert.
+     * @param string $contentType  The content type of the content.
+     *
+     * @return array  Two-element array with the converted content and the
+     *                (possibly changed) new content type.
+     */
+    function convertClient2Server($content, $contentType)
+    {
+        list($content, $contentType) =
+            parent::convertClient2Server($content, $contentType);
+
+        /* At least the Nokia E series seems to prefix category values with
+         * X-, see bugs #6849 and #7824. */
+        $di = $_SESSION['SyncML.state']->deviceInfo;
+        if ($di->Mod[0] == 'E') {
+            $content = preg_replace('/(\r\n|\r|\n)CATEGORIES:X-/',
+                                    '\1CATEGORIES:', $content, 1);
+        }
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\nInput converted for server ($contentType):\n$content\n");
+
+        return array($content, $contentType);
+    }
+
+    function convertServer2Client($content, $contentType, $database)
+    {
+        $database = $GLOBALS['backend']->_normalize($database);
+
+        list($content, $contentType, $encodingType) =
+            parent::convertServer2Client($content, $contentType, $database);
+
+        $content = preg_replace('/(\r\n|\r|\n)PHOTO;ENCODING=b[^:]*:(.+?)(\r\n|\r|\n)/',
+                                '\1PHOTO;ENCODING=BASE64:\1\2\1\1',
+                                $content, 1);
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\nOutput converted for client ($contentType):\n$content\n");
+
+        return array($content, $contentType);
+    }
+
+    function handleTasksInCalendar()
+    {
+        return true;
+    }
+
+    /**
+     * Some devices accept datetimes only in local time format:
+     * DTSTART:20061222T130000
+     * instead of the more robust (and default) UTC time:
+     * DTSTART:20061222T110000Z
+     */
+    function useLocalTime()
+    {
+        return true;
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Device/P800.php b/framework/SyncML/SyncML/Device/P800.php
new file mode 100644 (file)
index 0000000..78ece7c
--- /dev/null
@@ -0,0 +1,154 @@
+<?php
+/**
+ * P800/P900/P910:
+ * ---------------
+ * Charset:
+ * This device is able to handle UTF-8 and sends its XML packages in UTF8.
+ * However even though the XML itself is UTF-8, it expects the enclosed
+ * vcard-data to be ISO-8859-1 unless explicitly stated otherwise (using the
+ * CHARSET option, which is deprecated for VCARD 3.0)
+ *
+ * Encoding:
+ * String values are encoded "QUOTED-PRINTABLE"
+ *
+ * Other:
+ * This devices handles tasks and events in one database.
+ *
+ * As the P800 was the first device to work with package, most of the
+ * required conversions are in Device.php's default handling.
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/SyncML/SyncML/Device/P800.php,v 1.29 2009/01/06 17:49:50 jan Exp $
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device_P800 extends SyncML_Device {
+
+    /**
+     * Convert the content.
+     *
+     * @param string $content       The content to convert.
+     * @param string $contentType   The contentType of the content.
+     * @return array                array($newcontent, $newcontentType):
+     *                              the converted content and the
+     *                              (possibly changed) new ContentType.
+     */
+    function convertClient2Server($content, $contentType)
+    {
+        list($content, $contentType) =
+            parent::convertClient2Server($content, $contentType);
+
+        /* P800 sends categories as "X-Category". Remove the "X-".
+         * @todo: This hack only works with a single category. */
+        $content = preg_replace('/(\r\n|\r|\n)CATEGORIES:X-/', '\1CATEGORIES:',
+                                $content, 1);
+
+        /* P800 sends all day events as s.th. like
+         * DTSTART:20050505T000000Z^M
+         * DTEND:20050505T240000Z^M
+         * This is no longer an all day event when converted to local timezone.
+         * So manually handle this. */
+        if (preg_match('/(\r\n|\r|\n)DTSTART:.*T000000Z(\r\n|\r|\n)/',
+                       $content) &&
+            preg_match('/(\r\n|\r|\n)DTEND:(\d\d\d\d)(\d\d)(\d\d)T240000Z(\r\n|\r|\n)/',
+                       $content, $m)) {
+            $content = preg_replace(
+                '/(\r\n|\r|\n)DTSTART:(.*)T000000Z(\r\n|\r|\n)/',
+                "$1DTSTART;VALUE=DATE:$2$3", $content);
+            /* End timestamp must be converted to next day's date. Or maybe
+             * not? */
+            $s = date('Ymd', mktime(0, 0, 0, $m[3], $m[4], $m[2]) /* + 24*3600 */);
+            $content = preg_replace(
+                '/(\r\n|\r|\n)DTEND:(.*)T240000Z(\r\n|\r|\n)/',
+                "$1DTEND;VALUE=DATE:$s$3", $content);
+        }
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\ninput converted for server ($contentType):\n$content\n");
+
+        return array($content, $contentType);
+    }
+
+    /**
+     * Converts the content from the backend to a format suitable for the
+     * client device.
+     *
+     * Strips the uid (primary key) information as client and server might use
+     * different ones.
+     *
+     * @param string $content      The content to convert
+     * @param string $contentType  The content type of content as returned
+     *                             from the backend
+     * @param string $database     The server database URI.
+     *
+     * @return array  Three-element array with the converted content, the
+     *                (possibly changed) new content type, and encoding type
+     *                (like b64 as used by Funambol).
+     */
+    function convertServer2Client($content, $contentType, $database)
+    {
+        list($content, $contentType, $encodingType) =
+            parent::convertServer2Client($content, $contentType, $database);
+
+        /* Convert all day events. */
+        if (preg_match('/(\r\n|\r|\n)DTSTART:(\d{8})T000000/',
+                       $content)
+            && preg_match('/(\r\n|\r|\n)DTEND:(\d\d\d\d)(\d\d)(\d\d)T235959/',
+                          $content, $m)) {
+            /* @TODO: This is for P990. Check if it's different for P900.
+             * This might require T000000Z rather than T000000 */
+
+            /* The P990 seems to require this to recognize an entry as all day: */
+            $a = $m[1] . 'X-EPOCAGENDAENTRYTYPE:EVENT';
+            $content = preg_replace('/(\r\n|\r|\n)DTSTART:(\d{8})T000000/',
+                                    "$a$1DTSTART:$2T000000", $content);
+            /* End date must be converted to timestamp. */
+            $s = date('Ymd', mktime(0, 0, 0, $m[3], $m[4]+1, $m[2]));
+            $content = preg_replace('/(\r\n|\r|\n)DTEND:(\d{8})T235959/',
+                                    "$1DTEND:${s}T000000", $content);
+        }
+
+        $l = "\noutput converted for client ($contentType):\n" . $content . "\n";
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_DATA, $l);
+
+        return array($content, $contentType, $encodingType);
+    }
+
+    /**
+     * Some devices like the Sony Ericsson P800/P900/P910 handle vtodos (tasks)
+     * and vevents in the same "calendar" sync.
+     * This requires special actions on our side as we store this in different
+     * databases (nag and kronolith).
+     * This function could directly return true but tries to be a bit more
+     * generic so it might work for other phones as well.
+     */
+    function handleTasksInCalendar()
+    {
+        $di = $_SESSION['SyncML.state']->deviceInfo;
+
+        if (isset($di->CTCaps['text/x-vcalendar']) &&
+            !empty($di->CTCaps['text/x-vcalendar']['BEGIN']->ValEnum['VEVENT']) &&
+            !empty($di->CTCaps['text/x-vcalendar']['BEGIN']->ValEnum['VTODO'])) {
+            return true;
+        }
+
+        return parent::handleTasksInCalendar();
+    }
+
+    /**
+     * Send individual status response for each Add,Delete,Replace.
+     * The P800 class of devices seem to have trouble with too many
+     * status responses. So omit them for these (and only these),
+     */
+    function omitIndividualSyncStatus()
+    {
+        return true;
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Device/Sync4JMozilla.php b/framework/SyncML/SyncML/Device/Sync4JMozilla.php
new file mode 100755 (executable)
index 0000000..6d1c356
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * The SyncML_Device_Sync4JMozilla:: class provides functionality that is
+ * specific to the Sync4JMozilla Plugin. See
+ * http://sourceforge.net/projects/sync4jmozilla/
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/SyncML/SyncML/Device/Sync4JMozilla.php,v 1.7 2009/01/06 17:49:50 jan Exp $
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device_Sync4JMozilla extends SyncML_Device {
+
+    /**
+     * Converts the content from the backend to a format suitable for the
+     * client device.
+     *
+     * Strips the uid (primary key) information as client and server might use
+     * different ones.
+     *
+     * @param string $content      The content to convert
+     * @param string $contentType  The content type of content as returned
+     *                             from the backend
+     * @param string $database     The server database URI.
+     *
+     * @return array  Three-element array with the converted content, the
+     *                (possibly changed) new content type, and encoding type
+     *                (like b64 as used by Funambol).
+     */
+    function convertServer2Client($content, $contentType, $database)
+    {
+        list($content, $contentType, $encodingType) =
+            parent::convertServer2Client($content, $contentType, $database);
+
+
+        /* The plugin does currently not handle lines that are both folded
+         * and QUOTED-PRINTABLE encoded. Like this one with a note "abc":
+         * NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+         * a=
+         * bc
+         */
+
+        if (preg_match_all('/\r\n[^:]*ENCODING=QUOTED-PRINTABLE[^:]*:.*?=\r\n.*?[^=](?=\r\n)/mis', $content, $m)) {
+            foreach($m[0] as $v) {
+                /* Remove line folding */
+                $content = str_replace($v,str_replace("=\r\n", '', $v), $content);
+            }
+        }
+        $l = "\noutput converted for client ($contentType):\n" . $content . "\n";
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_DATA, $l);
+
+        return array($content, $contentType, $encodingType);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Device/Sync4j.php b/framework/SyncML/SyncML/Device/Sync4j.php
new file mode 100644 (file)
index 0000000..9e67236
--- /dev/null
@@ -0,0 +1,1254 @@
+<?php
+/**
+ * @package SyncML
+ *
+ * $Horde: framework/SyncML/SyncML/Device/Sync4j.php,v 1.73 2009/10/26 18:03:46 jan Exp $
+ */
+
+/** Horde_Date */
+require_once 'Horde/Date.php';
+
+/** Horde_iCalendar */
+require_once 'Horde/iCalendar.php';
+
+/**
+ * Sync4j (www.sync4j.org)
+ *
+ * The Sync4J outlook converter uses its native SIF format for data
+ * exchange. Conversion to text/vcalendar etc. is done by SifConverter.php The
+ * connector seems not support DevInf information, so SyncML_Device can only
+ * detect it by the decice ID: so in the connector configuration the device ID
+ * must be set to 'sc-pim-<type>' which should be the default anyhow.
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device_sync4j extends SyncML_Device {
+
+    function getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI)
+    {
+        $database = strtolower($serverSyncURI); // no string api needed here
+
+        // Code copied from parent function. But we must not use device
+        // mimetype from device information here as this would result in
+        // us asking the horde backend to provide crazy text/x-s4j-sifn
+        // stuff. Instead we ask backend for default type and convert it
+        // internally in this class (in convertServer2Client).
+        if (strpos($database, 'contact') !== false ||
+            strpos($database, 'card') !== false) {
+            return 'text/x-vcard';
+        } elseif (strpos($database, 'note') !== false ||
+                  strpos($database, 'memo') !== false) {
+            return 'text/x-vnote';
+        } elseif (strpos($database, 'task') !== false ||
+                  strpos($database, 'cal') !== false ||
+                  strpos($database, 'event') !== false) {
+            return 'text/calendar';
+        }
+
+        return parent::getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI);
+    }
+
+    /**
+     * Convert the content.
+     */
+    function convertClient2Server($content, $contentType)
+    {
+        list($content, $contentType) =
+            parent::convertClient2Server($content, $contentType);
+
+        switch ($contentType) {
+        case 'text/x-s4j-sifn' :
+        case 'text/x-sifn' :
+            $content = SyncML_Device_sync4j::sif2vnote($content);
+            $contentType = 'text/x-vnote';
+            break;
+
+        case 'text/x-s4j-sifc' :
+        case 'text/x-sifc' :
+            $content = SyncML_Device_sync4j::sif2vcard($content);
+            $contentType = 'text/x-vcard';
+            break;
+
+        case 'text/x-s4j-sife' :
+        case 'text/x-sife' :
+            $content = SyncML_Device_sync4j::sif2vevent($content);
+            $contentType = 'text/calendar';
+            break;
+
+        case 'text/x-s4j-sift' :
+        case 'text/x-sift' :
+            $content = SyncML_Device_sync4j::sif2vtodo($content);
+            $contentType = 'text/calendar';
+            break;
+        }
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\nInput converted for server ($contentType):\n$content\n");
+
+        return array($content, $contentType);
+    }
+
+    /**
+     * Converts the content from the backend to a format suitable for the
+     * client device.
+     *
+     * Strips the uid (primary key) information as client and server might use
+     * different ones.
+     *
+     * @param string $content      The content to convert
+     * @param string $contentType  The content type of content as returned
+     *                             from the backend
+     * @param string $database     The server database URI.
+     *
+     * @return array  Three-element array with the converted content, the
+     *                (possibly changed) new content type, and encoding type
+     *                (like b64 as used by Funambol).
+     */
+    function convertServer2Client($content, $contentType, $database)
+    {
+        $database = $GLOBALS['backend']->_normalize($database);
+
+        list($content, $contentType, $encodingType) =
+            parent::convertServer2Client($content, $contentType, $database);
+
+        switch ($contentType) {
+        case 'text/calendar' :
+        case 'text/x-vcalendar' :
+            switch($database) {
+            case 'calendar':
+                $content = SyncML_Device_sync4j::vevent2sif($content);
+                $content = base64_encode($content);
+                $contentType = 'text/x-s4j-sife';
+                break 2;
+            case 'tasks':
+                $content = SyncML_Device_sync4j::vtodo2sif($content);
+                $content = base64_encode($content);
+                $contentType = 'text/x-s4j-sift';
+                break 2;
+            }
+            break;
+
+        case 'text/x-vcard' :
+            $content = SyncML_Device_sync4j::vcard2sif($content);
+            $content = base64_encode($content);
+            $contentType = 'text/x-s4j-sifc';
+            break;
+
+        case 'text/x-vnote':
+        case 'text/plain':
+            $content = SyncML_Device_sync4j::vnote2sif($content);
+            $content = base64_encode($content);
+            $contentType = 'text/x-s4j-sifn';
+            break;
+        }
+
+        $l = "\nOutput converted for client ($contentType):\n" . base64_decode($content) . "\n";
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_DATA, $l);
+
+        return array($content, $contentType, 'b64');
+    }
+
+    /**
+     * Decodes a sif xml string to an associative array.
+     *
+     * Quick hack to convert from text/vcard and text/vcalendar to
+     * Sync4J's proprietery sif datatypes and vice versa.  For details
+     * about the sif format see the appendix of the developer guide on
+     * www.sync4j.org.
+     *
+     * @access private
+     *
+     * @param string $sif  A sif string like <k1>v1&gt;</k1><k2>v2</k2>
+     *
+     * @return array  Assoc array in utf8 like array ('k1' => 'v1>', 'k2' => 'v2');
+     */
+    function sif2array($sif)
+    {
+        $r = array();
+        if (preg_match_all('/<([^>]*)>([^<]*)<\/\1>/si', $sif, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $match) {
+                if (isset($r[$match[1]])) {
+                    if (!is_array($r[$match[1]])) {
+                        $r[$match[1]] = array($r[$match[1]]);
+                    }
+                    $r[$match[1]][] = html_entity_decode($match[2]);
+                } else {
+                    $r[$match[1]] = html_entity_decode($match[2]);
+                }
+            }
+        }
+        return $r;
+    }
+
+    /**
+     * Converts a hash to a SIF XML structure.
+     *
+     * @param array $array  A hash.
+     * @param string $pre   A prefix string for the XML result.
+     * @param string $post  A suffix string for the XML result.
+     *
+     * @return string  The resulting XML string.
+     */
+    function array2sif($array, $pre = '', $post = '')
+    {
+        $xml = $pre;
+        foreach ($array as $key => $value) {
+            if (is_a($value, 'PEAR_Error')) {
+                continue;
+            }
+            if (is_array($value)) {
+                if (is_array($value[0])) {
+                    $subxml = '';
+                    foreach ($value as $val) {
+                        $subkey = key($val);
+                        $subxml .= '<' . $subkey . '>'
+                            . htmlspecialchars($val[$subkey])
+                            . '</' . $subkey . '>';
+                    }
+                    $xml .= '<' . $key . '>' . $subxml . '</' . $key . '>';
+                    continue;
+                }
+                $value = $value[0];
+            }
+            $xml .= '<' . $key . '>' . htmlspecialchars($value) . '</' . $key . '>';
+        }
+        return $xml . $post;
+    }
+
+    function sif2vnote($sif)
+    {
+        $a = SyncML_Device_sync4j::sif2array($sif);
+
+        $iCal = new Horde_iCalendar();
+        $iCal->setAttribute('VERSION', '1.1');
+        $iCal->setAttribute('PRODID', '-//The Horde Project//SyncML//EN');
+        $iCal->setAttribute('METHOD', 'PUBLISH');
+
+        $vnote = &Horde_iCalendar::newComponent('vnote', $iCal);
+        $vnote->setAttribute('BODY', isset($a['Body']) ? $a['Body'] : '');
+        if (isset($a['Subject'])) {
+            $vnote->setAttribute('SUMMARY', $a['Subject']);
+        }
+        if (isset($a['Categories'])) {
+            $vnote->setAttribute('CATEGORIES', $a['Categories']);
+        }
+
+        return $vnote->exportvCalendar();
+    }
+
+    function sif2vcard($sif)
+    {
+        $a = SyncML_Device_sync4j::sif2array($sif);
+
+        $iCal = new Horde_iCalendar();
+        $iCal->setAttribute('VERSION', '3.0');
+        $iCal->setAttribute('PRODID', '-//The Horde Project//SyncML//EN');
+        $iCal->setAttribute('METHOD', 'PUBLISH');
+
+        $vcard = &Horde_iCalendar::newComponent('vcard', $iCal);
+
+        $map = array(
+            'FileAs' => array('FN'),
+            'NickName' => array('NICKNAME'),
+            'HomeTelephoneNumber' => array('TEL', array('TYPE' => 'HOME')),
+            'Home2TelephoneNumber' => array('TEL', array('TYPE' => 'HOME')),
+            'HomeFaxNumber' => array('TEL', array('TYPE' => 'HOME')),
+            'BusinessTelephoneNumber' => array('TEL', array('TYPE' => 'WORK')),
+            'Business2TelephoneNumber' => array('TEL', array('TYPE' => 'WORK')),
+            'BusinessFaxNumber' => array('TEL', array('TYPE' => 'FAX')),
+            'PrimaryTelephoneNumber' => array('TEL', array('TYPE' => 'PREF')),
+            'MobileTelephoneNumber' => array('TEL', array('TYPE' => 'CELL')),
+            'CarTelephoneNumber' => array('TEL', array('TYPE' => 'CAR')),
+            'PagerNumber' => array('TEL', array('TYPE' => 'PAGER')),
+            'OtherTelephoneNumber' => array('TEL'),
+            'OtherFaxNumber' => array('TEL'),
+            'Email1Address' => array('EMAIL'),
+            'Email2Address' => array('EMAIL', array('TYPE' => 'HOME')),
+            'Email3Address' => array('EMAIL', array('TYPE' => 'WORK')),
+            'HomeLabel' => array('LABEL', array('TYPE' => 'HOME')),
+            'BusinessLabel' => array('LABEL', array('TYPE' => 'WORK')),
+            'OtherLabel' => array('LABEL'),
+            'Profession' => array('ROLE'),
+            'JobTitle' => array('TITLE'),
+            'Body' => array('NOTE'),
+            'WebPage' => array('URL'),
+            'Birthday' => array('BDAY'),
+            'Categories' => array('CATEGORIES'),
+            'Timezone' => array('TZ'),
+            'Anniversary' => array('X-ANNIVERSARY'),
+            'Spouse' => array('X-SPOUSE'),
+            'Children' => array('X-CHILDREN'),
+        );
+        foreach ($map as $sif_value => $vcard_value) {
+            if (isset($a[$sif_value])) {
+                $vcard->setAttribute($vcard_value[0],
+                                     $a[$sif_value],
+                                     isset($vcard_value[1]) ? $vcard_value[1] : array());
+            }
+        }
+
+        $map = array(
+            array(
+                'N',
+                array(VCARD_N_FAMILY => 'LastName',
+                      VCARD_N_GIVEN  => 'FirstName',
+                      VCARD_N_ADDL   => 'MiddleName',
+                      VCARD_N_PREFIX => 'Title',
+                      VCARD_N_SUFFIX => 'Suffix'),
+                array(),
+                false),
+            array(
+                'ADR',
+                array(VCARD_ADR_POB      => 'HomeAddressPostOfficeBox',
+                      VCARD_ADR_EXTEND   => '',
+                      VCARD_ADR_STREET   => 'HomeAddressStreet',
+                      VCARD_ADR_LOCALITY => 'HomeAddressCity',
+                      VCARD_ADR_REGION   => 'HomeAddressState',
+                      VCARD_ADR_POSTCODE => 'HomeAddressPostalCode',
+                      VCARD_ADR_COUNTRY  => 'HomeAddressCountry'),
+                array('TYPE' => 'HOME'),
+                true),
+            array(
+                'ADR',
+                array(VCARD_ADR_POB      => 'BusinessAddressPostOfficeBox',
+                      VCARD_ADR_EXTEND   => '',
+                      VCARD_ADR_STREET   => 'BusinessAddressStreet',
+                      VCARD_ADR_LOCALITY => 'BusinessAddressCity',
+                      VCARD_ADR_REGION   => 'BusinessAddressState',
+                      VCARD_ADR_POSTCODE => 'BusinessAddressPostalCode',
+                      VCARD_ADR_COUNTRY  => 'BusinessAddressCountry'),
+                array('TYPE' => 'WORK'),
+                true),
+            array(
+                'ADR',
+                array(VCARD_ADR_POB      => 'OtherAddressPostOfficeBox',
+                      VCARD_ADR_EXTEND   => '',
+                      VCARD_ADR_STREET   => 'OtherAddressStreet',
+                      VCARD_ADR_LOCALITY => 'OtherAddressCity',
+                      VCARD_ADR_REGION   => 'OtherAddressState',
+                      VCARD_ADR_POSTCODE => 'OtherAddressPostalCode',
+                      VCARD_ADR_COUNTRY  => 'OtherAddressCountry'),
+                array(),
+                true),
+        );
+        foreach ($map as $struct) {
+            $values = array();
+            foreach ($struct[1] as $vcard_value => $sif_value) {
+                $values[$vcard_value] = isset($a[$sif_value]) ? $a[$sif_value] : '';
+            }
+            $check = array_flip($values);
+            if (count($check) > 1 || strlen(key($check))) {
+                $vcard->setAttribute($struct[0],
+                                     implode(';', $values),
+                                     $struct[2],
+                                     $struct[3],
+                                     $values);
+            }
+        }
+
+        $org = array();
+        if (isset($a['CompanyName'])) {
+            $org[] = $a['CompanyName'];
+            if (isset($a['Department'])) {
+                $org[] = $a['Department'];
+            }
+        }
+        if (count($org)) {
+            $vcard->setAttribute('ORG', null, array(), true, $org);
+        }
+
+        return $vcard->exportvCalendar();
+    }
+
+    function sif2vevent($sif)
+    {
+        $a = SyncML_Device_sync4j::sif2array($sif);
+
+        $iCal = new Horde_iCalendar();
+        $iCal->setAttribute('PRODID', '-//The Horde Project//SyncML//EN');
+        $iCal->setAttribute('METHOD', 'PUBLISH');
+
+        $vEvent = &Horde_iCalendar::newComponent('vevent', $iCal);
+        $vEvent->setAttribute('DTSTAMP', time());
+
+        $map = array('Subject' => 'SUMMARY',
+                     'Body' => 'DESCRIPTION',
+                     'Categories' => 'CATEGORIES',
+                     'Location' => 'LOCATION');
+        foreach ($map as $source => $target) {
+            if (!empty($a[$source])) {
+                $vEvent->setAttribute($target, $a[$source]);
+            }
+        }
+
+        if ($a['AllDayEvent'] == 1) {
+            // Not exactly correct, we ignore the start and end time of
+            // all-day events and simple assume that the client had set them
+            // correctly to 0:00.
+            $startTime = $iCal->_parseDateTime($a['Start']);
+            $vEvent->setAttribute('DTSTART',
+                                  array('year' => date('Y', $startTime),
+                                        'month' => date('m', $startTime),
+                                        'mday' => date('d', $startTime)),
+                                  array('VALUE' => 'DATE'));
+            $t = $iCal->_parseDateTime($a['End']);
+            $d = new Horde_Date(array('year' => date('Y', $t),
+                                      'month' => date('m', $t),
+                                      'mday' => date('d', $t) + 1));
+            $d->correct();
+            $vEvent->setAttribute('DTEND',$d, array('VALUE' => 'DATE'));
+        } else {
+            $startTime = $iCal->_parseDateTime($a['Start']);
+            $vEvent->setAttribute('DTSTART', $startTime);
+            $vEvent->setAttribute('DTEND',
+                                  $iCal->_parseDateTime($a['End']));
+        }
+
+        if (isset($a['IsRecurring']) && $a['IsRecurring'] == 1) {
+            $interval = '';
+            switch ($a['RecurrenceType']) {
+            case 0:
+                /* olDaily */
+                if (!empty($a['DayOfWeekMask'])) {
+                    $freq = 'WEEKLY';
+                    $interval = ';INTERVAL=1';
+                } else {
+                    $freq = 'DAILY';
+                    $interval = ';INTERVAL=' . $a['Interval'];
+                }
+                break;
+            case 1:
+                /* olWeekly */
+                $freq = 'WEEKLY';
+                $interval = ';INTERVAL=' . $a['Interval'];
+                break;
+            case 2:
+                /* olMonthly */
+                $freq = 'MONTHLY';
+                $interval = ';INTERVAL=' . $a['Interval'];
+                break;
+            case 3:
+                /* olMonthNth */
+                $freq = 'MONTHLY';
+                $interval = ';INTERVAL=' . $a['Interval'];
+                break;
+            case 5:
+                /* olYearly */
+                $freq = 'YEARLY';
+                $interval = ';INTERVAL=' . $a['Interval'];
+                break;
+            case 6:
+                /* olYearNth */
+                $freq = 'YEARLY';
+                $interval = ';INTERVAL=' . $a['Interval'];
+                break;
+            }
+            $rrule = 'FREQ=' . $freq;
+            if (isset($a['Occurrences'])) {
+                $rrule .= ';COUNT=' . $a['Occurrences'];
+            } elseif (!isset($a['NoEndDate']) || $a['NoEndDate'] != 1) {
+                $rrule .= ';UNTIL=' . $a['PatternEndDate'];
+            }
+            $rrule .= $interval;
+            if (!empty($a['DayOfMonth'])) {
+                $rrule .= ';BYMONTHDAY=' . $a['DayOfMonth'];
+            }
+            if (!empty($a['MonthOfYear'])) {
+                $rrule .= ';BYMONTH=' . $a['MonthOfYear'];
+            }
+            if (!empty($a['DayOfWeekMask'])) {
+                $rrule .= ';BYDAY=';
+                $icaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+                for ($i = $flag = 0; $i <= 7 ; ++$i) {
+                    if (pow(2, $i) & $a['DayOfWeekMask']) {
+                        if ($flag) {
+                            $rrule .= ',';
+                        }
+                        $rrule .= $icaldays[$i];
+                        $flag = true;
+                    }
+                }
+            }
+            $vEvent->setAttribute('RRULE', $rrule);
+        }
+
+        if (isset($a['ExcludeDate'])) {
+            $dates = array();
+            if (!is_array($a['ExcludeDate'])) {
+                $dates[] = $a['AllDayEvent'] == 1
+                    ? $iCal->_parseDate($a['ExcludeDate'])
+                    : $iCal->_parseDateTime($a['ExcludeDate']);
+            } else {
+                foreach ($a['ExcludeDate'] as $date) {
+                    $dates[] = $a['AllDayEvent'] == 1
+                        ? $iCal->_parseDate($date)
+                        : $iCal->_parseDateTime($date);
+                }
+            }
+            if ($a['AllDayEvent'] == 1) {
+                $vEvent->setAttribute('EXDATE', $dates, array('VALUE' => 'DATE'));
+            } else {
+                $vEvent->setAttribute('EXDATE', $dates);
+            }
+        }
+
+        if (isset($a['ReminderSet']) && $a['ReminderSet'] == 1) {
+            $vEvent->setAttribute('AALARM', $startTime - $a['ReminderMinutesBeforeStart'] * 60);
+        }
+
+        if (isset($a['BusyStatus'])) {
+            switch ($a['BusyStatus']) {
+            case 0:
+                /* olFree - FREE is not a iCalendar standard value. */
+                $vEvent->setAttribute('STATUS', 'FREE');
+                break;
+            case 1:
+                /* olTentative */
+                $vEvent->setAttribute('STATUS', 'TENTATIVE');
+                break;
+            case 2:
+                /* olBusy */
+            case 3:
+                /* olOutOfOffice */
+                $vEvent->setAttribute('STATUS', 'CONFIRMED');
+                break;
+            }
+        }
+
+        if (isset($a['Sensitivity'])) {
+            switch ($a['Sensitivity']) {
+            case 0:
+                /* olNormal - FREE is not a iCalendar standard value. */
+                $vEvent->setAttribute('CLASS', 'PUBLIC');
+                break;
+            case 1:
+                /* olPersonal */
+            case 2:
+                /* olPrivate */
+                $vEvent->setAttribute('CLASS', 'PRIVATE');
+                break;
+            case 3:
+                /* olConfidential */
+                $vEvent->setAttribute('CLASS', 'CONFIDENTIAL');
+                break;
+            }
+        }
+
+        return $vEvent->exportvCalendar();
+    }
+
+    function sif2vtodo($sif)
+    {
+        $a = SyncML_Device_sync4j::sif2array($sif);
+
+        $iCal = new Horde_iCalendar();
+        $iCal->setAttribute('PRODID', '-//The Horde Project//SyncML//EN');
+        $iCal->setAttribute('METHOD', 'PUBLISH');
+
+        $vtodo = &Horde_iCalendar::newComponent('vtodo', $iCal);
+
+        $vtodo->setAttribute('SUMMARY', $a['Subject']);
+        $vtodo->setAttribute('DESCRIPTION', $a['Body']);
+        if ($a['Importance'] == 0) {
+            $vtodo->setAttribute('PRIORITY', 5);
+        } elseif ($a['Importance'] == 2) {
+            $vtodo->setAttribute('PRIORITY', 1);
+        } else {
+            $vtodo->setAttribute('PRIORITY', 3);
+        }
+        if (!empty($a['StartDate']) && $a['StartDate'] != '45001231T230000Z') {
+            $vtodo->setAttribute('DTSTART', $iCal->_parseDateTime($a['StartDate']));
+        }
+        $dueSet = false;
+        if (!empty($a['DueDate']) && $a['DueDate'] != '45001231T230000Z') {
+            $vtodo->setAttribute('DUE', $iCal->_parseDateTime($a['DueDate']));
+            $dueSet = true;
+        }
+        if (!empty($a['ReminderSet'])) {
+            if (!$dueSet) {
+                $vtodo->setAttribute('DUE', $iCal->_parseDateTime($a['ReminderTime']));
+            }
+            $vtodo->setAttribute('AALARM', $iCal->_parseDateTime($a['ReminderTime']));
+        }
+        if (!empty($a['Complete'])) {
+            $vtodo->setAttribute('STATUS', 'COMPLETED');
+        }
+        $vtodo->setAttribute('CATEGORIES', isset($a['Categories']) ? $a['Categories'] : '');
+        if (isset($a['Sensitivity'])) {
+            switch ($a['Sensitivity']) {
+            case 0:
+                /* olNormal */
+                $vtodo->setAttribute('CLASS', 'PUBLIC');
+                break;
+            case 1:
+                /* olPersonal */
+            case 2:
+                /* olPrivate */
+                $vtodo->setAttribute('CLASS', 'PRIVATE');
+                break;
+            case 3:
+                /* olConfidential */
+                $vtodo->setAttribute('CLASS', 'CONFIDENTIAL');
+                break;
+            }
+        }
+
+        return $vtodo->exportvCalendar();
+    }
+
+    function vnote2sif($vnote)
+    {
+        $iCal = new Horde_iCalendar();
+        if (!$iCal->parsevCalendar($vnote)) {
+            // handle plain text:
+            $a = array('Body' => $vnote);
+        } else {
+            $components = $iCal->getComponents();
+            if (!is_array($components) || count($components) == 0) {
+                $a = array(
+                    'Body' => _("Error converting notes."));
+            } else {
+                $a = array(
+                    'Body' => $components[0]->getAttribute('BODY'),
+                    'Categories' => $components[0]->getAttribute('CATEGORIES'));
+                $sum = $components[0]->getAttribute('SUMMARY');
+                if (!is_a($sum, 'PEAR_Error')) {
+                    $a['Subject'] = $sum;
+                }
+            }
+        }
+
+        return SyncML_Device_sync4j::array2sif($a, '<note>', '</note>');
+    }
+
+    function vcard2sif($vcard)
+    {
+        $iCal = new Horde_iCalendar();
+        if (!$iCal->parsevCalendar($vcard)) {
+            // @TODO: NEVER use die() in a library.
+            die("There was an error importing the data.");
+        }
+
+        $components = $iCal->getComponents();
+
+        switch (count($components)) {
+        case 0:
+            // @TODO: NEVER use die() in a library.
+            die("No data was found.");
+
+        case 1:
+            $content = $components[0];
+            break;
+
+        default:
+            // @TODO: NEVER use die() in a library.
+            die("Multiple components found; only one is supported.");
+        }
+
+        // from here on, the code is taken from
+        // Turba_Driver::toHash, v 1.65 2005/03/12
+        // and modified for the Sync4J attribute names.
+        $attr = $content->getAllAttributes();
+        foreach ($attr as $item) {
+            switch ($item['name']) {
+            case 'FN':
+                $hash['FileAs'] = $item['value'];
+                break;
+
+            case 'N':
+                $name = $item['values'];
+                $hash['LastName'] = $name[VCARD_N_FAMILY];
+                $hash['FirstName'] = $name[VCARD_N_GIVEN];
+                $hash['MiddleName'] = $name[VCARD_N_ADDL];
+                $hash['Title'] = $name[VCARD_N_PREFIX];
+                $hash['Suffix'] = $name[VCARD_N_SUFFIX];
+                break;
+
+            case 'NICKNAME':
+                $hash['NickName'] = $item['value'];
+                break;
+
+            // For vCard 3.0.
+            case 'ADR':
+                if (isset($item['params']['TYPE'])) {
+                    if (!is_array($item['params']['TYPE'])) {
+                        $item['params']['TYPE'] = array($item['params']['TYPE']);
+                    }
+                } else {
+                    $item['params']['TYPE'] = array();
+                    if (isset($item['params']['WORK'])) {
+                        $item['params']['TYPE'][] = 'WORK';
+                    }
+                    if (isset($item['params']['HOME'])) {
+                        $item['params']['TYPE'][] = 'HOME';
+                    }
+                }
+
+                $address = $item['values'];
+                foreach ($item['params']['TYPE'] as $adr) {
+                    switch (Horde_String::upper($adr)) {
+                    case 'HOME':
+                        $prefix = 'HomeAddress';
+                        break;
+
+                    case 'WORK':
+                        $prefix = 'BusinessAddress';
+                        break;
+
+                    default:
+                        $prefix = 'HomeAddress';
+                    }
+
+                    if ($prefix) {
+                        $hash[$prefix . 'Street'] =
+                            isset($address[VCARD_ADR_STREET])
+                            ? $address[VCARD_ADR_STREET]
+                            : null;
+                        $hash[$prefix . 'City'] =
+                            isset($address[VCARD_ADR_LOCALITY])
+                            ? $address[VCARD_ADR_LOCALITY]
+                            : null;
+                        $hash[$prefix . 'State'] =
+                            isset($address[VCARD_ADR_REGION])
+                            ? $address[VCARD_ADR_REGION]
+                            : null;
+                        $hash[$prefix . 'PostalCode'] =
+                            isset($address[VCARD_ADR_POSTCODE])
+                            ? $address[VCARD_ADR_POSTCODE]
+                            : null;
+                        $hash[$prefix . 'Country'] =
+                            isset($address[VCARD_ADR_COUNTRY])
+                            ? $address[VCARD_ADR_COUNTRY]
+                            : null;
+                        $hash[$prefix . 'PostOfficeBox'] =
+                            isset($address[VCARD_ADR_POB])
+                            ? $address[VCARD_ADR_POB]
+                            : null;
+                    }
+                }
+                break;
+
+            case 'TEL':
+                if (isset($item['params']['FAX'])) {
+                    if (isset($item['params']['WORK'])) {
+                        $hash['BusinessFaxNumber'] = $item['value'];
+                    } elseif (isset($item['params']['HOME'])) {
+                        $hash['HomeFaxNumber'] = $item['value'];
+                    } else {
+                        $hash['OtherFaxNumber'] = $item['value'];
+                    }
+                } elseif (isset($item['params']['TYPE'])) {
+                    if (!is_array($item['params']['TYPE'])) {
+                        $item['params']['TYPE'] = array($item['params']['TYPE']);
+                    }
+                    // For vCard 3.0.
+                    foreach ($item['params']['TYPE'] as $tel) {
+                        if (Horde_String::upper($tel) == 'WORK') {
+                            $hash['BusinessTelephoneNumber'] = $item['value'];
+                        } elseif (Horde_String::upper($tel) == 'HOME') {
+                            $hash['HomeTelephoneNumber'] = $item['value'];
+                        } elseif (Horde_String::upper($tel) == 'CELL') {
+                            $hash['MobileTelephoneNumber'] = $item['value'];
+                        } elseif (Horde_String::upper($tel) == 'FAX') {
+                            $hash['BusinessFaxNumber'] = $item['value'];
+                        }
+                    }
+                } else {
+                    if (isset($item['params']['HOME'])) {
+                        $hash['HomeTelephoneNumber'] = $item['value'];
+                    } elseif (isset($item['params']['WORK'])) {
+                        $hash['BusinessTelephoneNumber'] = $item['value'];
+                    } elseif (isset($item['params']['CELL'])) {
+                        $hash['MobileTelephoneNumber'] = $item['value'];
+                    } elseif (!isset($hash['HomeTelephoneNumber'])) {
+                        $hash['HomeTelephoneNumber'] = $item['value'];
+                    }
+                }
+                break;
+
+            case 'EMAIL':
+                $email_set = false;
+                if (isset($item['params']['HOME']) && (!isset($hash['Email2Address']) ||
+                    isset($item['params']['PREF']))) {
+                   $hash['Email2Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
+                   $email_set = true;
+                } elseif (isset($item['params']['WORK']) && (!isset($hash['Email3Address']) || 
+                          isset($item['params']['PREF']))) {
+                   $hash['Email3Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
+                   $email_set = true;
+                } elseif (isset($item['params']['TYPE'])) {
+                   if (!is_array($item['params']['TYPE'])) {
+                      $item['params']['TYPE'] = array($item['params']['TYPE']);
+                   }
+                   if (in_array('HOME', $item['params']['TYPE']) && 
+                       (!isset($hash['Email2Address']) || in_array('PREF', $item['params']['TYPE']))) {
+                      $hash['Email2Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
+                      $email_set = true;
+                   } elseif (in_array('WORK', $item['params']['TYPE']) &&
+                             (!isset($hash['Email3Address']) || in_array('PREF', $item['params']['TYPE']))) {
+                      $hash['Email3Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
+                      $email_set = true;
+                   }
+                }
+                if (!$email_set && (!isset($hash['Email1Address']) || isset($item['params']['PREF']))) {
+                   $hash['Email1Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
+                }
+                break;
+
+            case 'TITLE':
+                $hash['JobTitle'] = $item['value'];
+                break;
+
+            case 'ORG':
+                $values = preg_split('/(?<!\\\\);/', trim($item['value']));
+                $hash['CompanyName'] = $values[0];
+                $hash['Department'] = isset($values[1]) ? $values[1] : '';
+                break;
+
+            case 'NOTE':
+                $hash['Body'] = $item['value'];
+                break;
+
+            case 'URL':
+                $hash['WebPage'] = $item['value'];
+                break;
+
+            case 'BDAY':
+                if (is_array($item['value'])) {
+                    $hash['Birthday'] = sprintf('%04d-%02d-%02d',
+                                                $item['value']['year'],
+                                                $item['value']['month'],
+                                                $item['value']['mday']);
+                }
+                break;
+
+            case 'X-ANNIVERSARY':
+                if (is_array($item['value'])) {
+                    $hash['Anniversary'] = sprintf('%04d-%02d-%02d',
+                                                $item['value']['year'],
+                                                $item['value']['month'],
+                                                $item['value']['mday']);
+                }
+                break;
+
+            case 'X-SPOUSE':
+                $hash['Spouse'] = $item['value'];
+                break;
+
+            case 'X-CHILDREN':
+                $hash['Children'] = $item['value'];
+                break;
+
+            case 'CATEGORIES':
+                $hash['Categories'] = $item['value'];
+                break;
+            }
+        }
+
+        return SyncML_Device_sync4j::array2sif(
+            $hash,
+            '<?xml version="1.0"?><contact>',
+            '</contact>');
+    }
+
+    function vevent2sif($vcard)
+    {
+        /* Some special handling for all-day vEvents that are not passed
+         * as TYPE=DATE (TYPE=DATE does not exist for vCalendar 1.0) */
+        if (preg_match('/(\r\n|\r|\n)DTSTART:.*T000000(\r\n|\r|\n)/', $vcard)) {
+            if (preg_match('/(\r\n|\r|\n)DTEND:(\d\d\d\d)(\d\d)(\d\d)T235959(\r\n|\r|\n)/', $vcard, $m)) {
+                $vcard = preg_replace('/(\r\n|\r|\n)DTSTART:(.*)T000000(\r\n|\r|\n)/',
+                                      "$1DTSTART;VALUE=DATE:$2$3", $vcard);
+                $vcard = preg_replace('/(\r\n|\r|\n)DTEND:(.*)T235959(\r\n|\r|\n)/',
+                                      "$1DTEND;VALUE=DATE:$2$3", $vcard);
+            }
+            // @TODO: else: handle case with DTEND= T240000
+        }
+        $iCal = new Horde_iCalendar();
+        if (!$iCal->parsevCalendar($vcard)) {
+            // @TODO: NEVER use die() in a library.
+            die("There was an error importing the data.");
+        }
+
+        $components = $iCal->getComponents();
+
+        switch (count($components)) {
+        case 0:
+            // @TODO: NEVER use die() in a library.
+            die("No data was found.");
+
+        case 1:
+            $content = $components[0];
+            break;
+
+        default:
+            // @TODO: NEVER use die() in a library.
+            die("Multiple components found; only one is supported.");
+        }
+
+        $hash = array('ReminderSet' => 0,
+                      'IsRecurring' => 0,
+                      'BusyStatus' => 2);
+        $alarm = $end = null;
+        $start = $content->getAttribute('DTSTART');
+        if ($start) {
+            if (!empty($start['params']['VALUE']) &&
+                $start['params']['VALUE'] == 'DATE') {
+                $hash['AllDayEvent'] = 1;
+                $hash['Start'] = sprintf('%04d-%02d-%02d',
+                                         $start['value']['year'],
+                                         $start['value']['month'],
+                                         $start['value']['mday']);
+                $start = mktime(0, 0, 0,
+                                $start['value']['month'],
+                                $start['value']['mday'],
+                                $start['value']['year']);
+            } else {
+                $hash['AllDayEvent'] = 0;
+                $hash['Start'] = Horde_iCalendar::_exportDateTime($start);
+                $start = $start;
+            }
+        }
+
+        foreach ($content->getAllAttributes() as $item) {
+            $GLOBALS['backend']->logMessage(
+                sprintf('Sync4j for name %s, value %s',
+                        $item['name'],
+                        is_string($item['value'])
+                        ? $item['value'] : var_export($item['value'], true)),
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            switch (Horde_String::upper($item['name'])) {
+            case 'DTSTART':
+                break;
+
+            case 'DTEND':
+                if (!empty($item['params']['VALUE']) &&
+                    $item['params']['VALUE'] == 'DATE') {
+                    $hash['AllDayEvent'] = 1;
+                    $date = new Horde_Date($item['value']['year'],
+                                           $item['value']['month'],
+                                           $item['value']['mday']);
+                    $date->mday--;
+                    $hash['End'] = $date->format('Y-m-d');
+                    $end = $date->datestamp();
+                } else {
+                    $hash['AllDayEvent'] = 0;
+                    $hash['End'] = Horde_iCalendar::_exportDateTime($item['value']);
+                    $end = $item['value'];
+                }
+                break;
+
+            case 'SUMMARY':
+                $hash['Subject'] = $item['value'];
+                break;
+
+            case 'DESCRIPTION':
+                $hash['Body'] = $item['value'];
+                break;
+
+            case 'LOCATION':
+                $hash['Location'] = $item['value'];
+                break;
+
+            case 'CATEGORIES':
+                $hash['Categories'] = $item['value'];
+                break;
+
+            case 'AALARM':
+                $hash['ReminderSet'] = 1;
+                $alarm = $item['value'];
+                break;
+
+            case 'STATUS':
+                switch (Horde_String::upper($item['value'])) {
+                case 'FREE':
+                case 'CANCELLED':
+                    $hash['BusyStatus'] = 0;
+                    break;
+
+                case 'TENTATIVE':
+                    $hash['BusyStatus'] = 1;
+                    break;
+
+                case 'CONFIRMED':
+                    $hash['BusyStatus'] = 2;
+                    break;
+                }
+                break;
+
+            case 'CLASS':
+                switch (Horde_String::upper($item['value'])) {
+                case 'PUBLIC':
+                    $hash['Sensitivity'] = 0;
+                    break;
+
+                case 'PRIVATE':
+                    $hash['Sensitivity'] = 2;
+                    break;
+
+                case 'CONFIDENTIAL':
+                    $hash['Sensitivity'] = 3;
+                    break;
+                }
+                break;
+
+            case 'RRULE':
+                // Parse the recurrence rule into keys and values.
+                $rdata = array();
+                $parts = explode(';', $item['value']);
+                foreach ($parts as $part) {
+                    list($key, $value) = explode('=', $part, 2);
+                    $rdata[Horde_String::upper($key)] = $value;
+                }
+
+                if (!isset($rdata['FREQ'])) {
+                    break;
+                }
+
+                $hash['IsRecurring'] = 1;
+
+                if (isset($rdata['BYDAY'])) {
+                    $maskdays = array('SU' => Horde_Date::MASK_SUNDAY,
+                                      'MO' => Horde_Date::MASK_MONDAY,
+                                      'TU' => Horde_Date::MASK_TUESDAY,
+                                      'WE' => Horde_Date::MASK_WEDNESDAY,
+                                      'TH' => Horde_Date::MASK_THURSDAY,
+                                      'FR' => Horde_Date::MASK_FRIDAY,
+                                      'SA' => Horde_Date::MASK_SATURDAY);
+                    $days = explode(',', $rdata['BYDAY']);
+                    $mask = 0;
+                    foreach ($days as $day) {
+                        $instance = (int)$day;
+                        $mask |= $maskdays[str_replace($instance, '', $day)];
+                    }
+                }
+
+                switch (Horde_String::upper($rdata['FREQ'])) {
+                case 'DAILY':
+                    $hash['RecurrenceType'] = 0;
+                    break;
+
+                case 'WEEKLY':
+                    $hash['RecurrenceType'] = 1;
+                    if (isset($rdata['BYDAY'])) {
+                        $hash['DayOfWeekMask'] = $mask;
+                    }
+                    break;
+
+                case 'MONTHLY':
+                    if (isset($rdata['BYDAY'])) {
+                        $hash['RecurrenceType'] = 3;
+                        $hash['Instance'] = $instance;
+                        $hash['DayOfWeekMask'] = $mask;
+                    } else {
+                        $hash['RecurrenceType'] = 2;
+                        $hash['DayOfMonth'] = date('j', $start);
+                    }
+                    break;
+
+                case 'YEARLY':
+                    if (isset($rdata['BYDAY'])) {
+                        $hash['RecurrenceType'] = 6;
+                        $hash['Instance'] = $instance;
+                        $hash['DayOfWeekMask'] = $mask;
+                    } else {
+                        $hash['RecurrenceType'] = 5;
+                        $hash['DayOfMonth'] = date('j', $start);
+                    }
+                    $hash['MonthOfYear'] = date('n', $start);
+                    break;
+                }
+
+                $hash['Interval'] = isset($rdata['INTERVAL'])
+                    ? $rdata['INTERVAL']
+                    : 1;
+
+                if (isset($rdata['UNTIL'])) {
+                    $hash['NoEndDate'] = 0;
+                    $hash['PatternEndDate'] = $rdata['UNTIL'];
+                } elseif (isset($rdata['COUNT'])) {
+                    $hash['NoEndDate'] = 0;
+                    $hash['Occurrences'] = $rdata['COUNT'];
+                } else {
+                    $hash['NoEndDate'] = 1;
+                }
+                break;
+
+            case 'EXDATE':
+                if (empty($hash['Exceptions'])) {
+                    $hash['Exceptions'] = array();
+                }
+                foreach ($item['values'] as $date) {
+                    if ($hash['AllDayEvent'] == 1) {
+                        $d = new Horde_Date(array('year' => $date['year'],
+                                                  'month' => $date['month'],
+                                                  'mday' => $date['mday'] + 1));
+                        $d->correct();
+                        $hash['Exceptions'][] = array('ExcludeDate' => $d->format('Y-m-d'));
+                    } else {
+                        $hash['Exceptions'][] = array('ExcludeDate' => Horde_iCalendar::_exportDate($date));
+                    }
+                }
+                break;
+            }
+        }
+
+        if (!empty($start)) {
+            if ($hash['ReminderSet'] && !empty($alarm) && $start != $alarm) {
+                $hash['ReminderMinutesBeforeStart'] = ($start - $alarm) / 60;
+            } else {
+                // Parse VALARM components.
+                foreach ($content->getComponents() as $component) {
+                    if (!is_a($component, 'Horde_iCalendar_valarm') ||
+                        is_a($trigger = $component->getAttribute('TRIGGER'), 'PEAR_Error') ||
+                        is_array($trigger) ||
+                        empty($trigger)) {
+                        continue;
+                    }
+                    $hash['ReminderSet'] = 1;
+                    $hash['ReminderMinutesBeforeStart'] = (-$trigger) / 60;
+                }
+            }
+        }
+
+        if (empty($hash['AllDayEvent']) && !empty($start) &&
+            !empty($end) && $start != $end) {
+            $hash['Duration'] = ($end - $start) / 60;
+            $GLOBALS['backend']->logMessage(
+                'Duration set to ' . $hash['Duration'],
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        }
+
+        return SyncML_Device_sync4j::array2sif(
+            $hash,
+            '<?xml version="1.0"?><appointment>',
+            '</appointment>');
+    }
+
+    function vtodo2sif($vcard)
+    {
+        $iCal = new Horde_iCalendar();
+        if (!$iCal->parsevCalendar($vcard)) {
+            return PEAR::raiseError('There was an error importing the data.');
+        }
+
+        $components = $iCal->getComponents();
+
+        switch (count($components)) {
+        case 0:
+            return PEAR::raiseError('No data was found');
+
+        case 1:
+            $content = $components[0];
+            break;
+
+        default:
+            return PEAR::raiseError('Multiple components found; only one is supported.');
+        }
+
+        $hash['Complete'] = 0;
+        $due = false;
+
+        $attr = $content->getAllAttributes();
+        foreach ($attr as $item) {
+            switch ($item['name']) {
+            case 'SUMMARY':
+                $hash['Subject'] = $item['value'];
+                break;
+
+            case 'DESCRIPTION':
+                $hash['Body'] = $item['value'];
+                break;
+
+            case 'PRIORITY':
+                if ($item['value'] == 1) {
+                    $hash['Importance'] = 2;
+                } elseif ($item['value'] == 5) {
+                    $hash['Importance'] = 0;
+                } else {
+                    $hash['Importance'] = 1;
+                }
+                break;
+
+            case 'DTSTART':
+                $hash['StartDate'] = Horde_iCalendar::_exportDateTime($item['value']);
+                break;
+
+            case 'DUE':
+                $hash['DueDate'] = Horde_iCalendar::_exportDateTime($item['value']);
+                $due = $item['value'];
+                break;
+
+            case 'AALARM':
+                $hash['ReminderTime'] = $item['value'];
+                $hash['ReminderSet'] = 1;
+                break;
+
+            case 'STATUS':
+                $hash['Complete'] = $item['value'] == 'COMPLETED' ? 1 : 0;
+                break;
+
+            case 'CATEGORIES':
+                $hash['Categories'] = $item['value'];
+                break;
+
+            case 'CLASS':
+                switch (Horde_String::upper($item['value'])) {
+                case 'PUBLIC':
+                    $hash['Sensitivity'] = 0;
+                    break;
+
+                case 'PRIVATE':
+                    $hash['Sensitivity'] = 2;
+                    break;
+
+                case 'CONFIDENTIAL':
+                    $hash['Sensitivity'] = 3;
+                    break;
+                }
+                break;
+            }
+        }
+
+        if ($due && !isset($hash['ReminderSet'])) {
+            // Parse VALARM components.
+            foreach ($content->getComponents() as $component) {
+                if (!is_a($component, 'Horde_iCalendar_valarm') ||
+                    is_a($trigger = $component->getAttribute('TRIGGER'), 'PEAR_Error') ||
+                    is_array($trigger) ||
+                    empty($trigger)) {
+                    continue;
+                }
+                $hash['ReminderSet'] = 1;
+                $hash['ReminderTime'] = Horde_iCalendar::_exportDateTime($due - $trigger);
+            }
+        }
+
+        return SyncML_Device_sync4j::array2sif(
+            $hash,
+            '<?xml version="1.0"?><task>',
+            '</task>');
+    }
+
+    /**
+     * Sync4j as of Funambol Outlook connector 3.0.15 can't deal
+     * with <![CDATA[ so omit it.
+     * The Funambol Sync4j client chokes on the cdata
+     * so for this device it has to be set to false. Syn4j uses base64
+     * encoding and so the problems with escaping does not occur.
+     */
+    function useCdataTag()
+    {
+        return false;
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Device/Synthesis.php b/framework/SyncML/SyncML/Device/Synthesis.php
new file mode 100644 (file)
index 0000000..9c178d8
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * The SyncML_Device_Synthesis:: class provides functionality that is
+ * specific to the Synthesis.ch SyncML clients.
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * $Horde: framework/SyncML/SyncML/Device/Synthesis.php,v 1.26 2009/01/06 17:49:50 jan Exp $
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @package SyncML
+ */
+class SyncML_Device_Synthesis extends SyncML_Device {
+
+    /**
+     * Converts the content from the backend to a format suitable for the
+     * client device.
+     *
+     * Strips the uid (primary key) information as client and server might use
+     * different ones.
+     *
+     * @param string $content      The content to convert
+     * @param string $contentType  The content type of content as returned
+     *                             from the backend
+     * @param string $database     The server database URI.
+     *
+     * @return array  Three-element array with the converted content, the
+     *                (possibly changed) new content type, and encoding type
+     *                (like b64 as used by Funambol).
+     */
+    function convertServer2Client($content, $contentType, $database)
+    {
+        list($content, $contentType, $encodingType) =
+            parent::convertServer2Client($content, $contentType, $database);
+
+        $di = $_SESSION['SyncML.state']->deviceInfo;
+        if (stristr($di->Mod,'palm') === false) {
+            // Some special priority handling is required. Synthesis uses
+            // 1 (high), 2 (medium), 3(low), at least for my windows mobile device.
+            // convert these to valid priority settings:
+            $content = preg_replace('/(\r\n|\r|\n)PRIORITY:[1-2](\r\n|\r|\n)/', '\1PRIORITY:1\2', $content, 1);
+            $content = preg_replace('/(\r\n|\r|\n)PRIORITY:[3](\r\n|\r|\n)/', '\1PRIORITY:2\2', $content, 1);
+            $content = preg_replace('/(\r\n|\r|\n)PRIORITY:[4-9](\r\n|\r|\n)/', '\1PRIORITY:3\2', $content, 1);
+        }
+        // Windows Mobile also expects DUE DATES like DUE:20060419T000000
+        if (preg_match('/(\r\n|\r|\n)DUE:(........T......Z)(\r\n|\r|\n)/',
+                $content,$m)) {
+            $m[2] = $this->UTC2LocalDate($m[2]);
+            $content = preg_replace('/(\r\n|\r|\n)DUE:(........T......Z)(\r\n|\r|\n)/',
+                '\1DUE:' . $m[2] . '\3', $content, 1);
+        }
+
+        $l = "\noutput converted for client ($contentType):\n" . $content . "\n";
+        $GLOBALS['backend']->logFile(SYNCML_LOGFILE_DATA, $l);
+
+        return array($content, $contentType, $encodingType);
+    }
+
+    /**
+     * Convert the content.
+     *
+     * @param string $content       The content to convert.
+     * @param string $contentType   The contentType of the content.
+     * @return array                array($newcontent, $newcontentType):
+     *                              the converted content and the
+     *                              (possibly changed) new ContentType.
+     */
+    function convertClient2Server($content, $contentType)
+    {
+        list($content, $contentType) =
+            parent::convertClient2Server($content, $contentType);
+
+        $di = $_SESSION['SyncML.state']->deviceInfo;
+        if (stristr($di->Mod, 'palm') === false) {
+            // Some special priority handling is required. Synthesis uses 1
+            // (high), 2 (medium), 3(low), at least for my windows mobile
+            // device.  convert these to valid priority settings:
+            $content = preg_replace('/(\r\n|\r|\n)PRIORITY:3(\r\n|\r|\n)/',
+                                    '\1PRIORITY:5\2', $content, 1);
+            $content = preg_replace('/(\r\n|\r|\n)PRIORITY:2(\r\n|\r|\n)/',
+                                    '\1PRIORITY:3\2', $content, 1);
+        }
+
+        $GLOBALS['backend']->logFile(
+            SYNCML_LOGFILE_DATA,
+            "\ninput converted for server ($contentType):\n$content\n");
+
+        return array($content, $contentType);
+
+    }
+
+
+    /* Static helper function: converts a UTC Timestamp like 20060418T220000Z
+     * into a local date like 20060419T000000. This is actually more than
+     * stripping the time part: we need to convert to local time first to ensure
+     * we get the right date!
+     */
+    function UTC2LocalDate($s)
+    {
+        $t = Horde_iCalendar::_parseDateTime($s);
+        return date('Ymd', $t) . 'T000000';
+    }
+}
diff --git a/framework/SyncML/SyncML/DeviceInfo.php b/framework/SyncML/SyncML/DeviceInfo.php
new file mode 100644 (file)
index 0000000..cda7081
--- /dev/null
@@ -0,0 +1,376 @@
+<?php
+/**
+ * SyncML_DeviceInfo represents a device information set according to the
+ * SyncML specification.
+ *
+ * A DeviceInfo object is created by SyncML_Command_Put from an appropriate
+ * XML message. SyncML_Command_Put directly populates the members variables.
+ *
+ * The current implementation should handle all DevInf 1.1 DTD elements
+ * except <DSMem> entries.
+ *
+ * $Horde: framework/SyncML/SyncML/DeviceInfo.php,v 1.20 2009/01/06 17:49:48 jan Exp $
+ *
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @author  Jan Schneider <jan@horde.org>
+ * @package SyncML
+ */
+class SyncML_DeviceInfo {
+
+    /**
+     * The major and minor version identifier of the Device Information DTD.
+     *
+     * @var string
+     */
+    var $VerDTD;
+
+    /**
+     * The name of the manufacturer of the device.
+     *
+     * @var string
+     */
+    var $Man;
+
+    /**
+     * The model name or model number of the device.
+     *
+     * @var string
+     */
+    var $Mod;
+
+    /**
+     * The OEM (Original Equipment Manufacturer) of the device.
+     *
+     * @var string
+     */
+    var $OEM;
+
+    /**
+     * The firmware version of the device.
+     *
+     * @var string
+     */
+    var $FwV;
+
+    /**
+     * The software version of the device.
+     *
+     * @var string
+     */
+    var $SwV;
+
+    /**
+     * The hardware version of the device.
+     *
+     * @var string
+     */
+    var $HwV;
+
+    /**
+     * The (globally unique) identifier of the source synchronization device.
+     *
+     * @var string
+     */
+    var $DevID;
+
+    /**
+     * The type of the source synchronization device.
+     *
+     * @var string
+     */
+    var $DevTyp;
+
+    /**
+     * Array of SyncML_DataStore objects.
+     *
+     * @var array
+     */
+    var $DataStores = array();
+
+    /**
+     * Multidimensional array that specifies the content type capabilities of
+     * the device.
+     *
+     * Example: array('text/x-vcard' => array('FN' => SyncML_Property))
+     *
+     * @var array
+     */
+    var $CTCaps;
+
+    /**
+     * The non-standard, experimental extensions supported by the device.
+     *
+     * A hash with <XNam> elements as keys and arrays of <XVal> elements as
+     * values.
+     * Example: array('X-Foo-Bar' => array(1, 'foo'))
+     *
+     * @var array
+     */
+    var $Exts;
+
+    /**
+     * Whether the device supports UTC based time.
+     *
+     * @var boolean
+     */
+    var $UTC;
+
+    /**
+     * Whether the device supports handling of large objects.
+     *
+     * @var boolean
+     */
+    var $SupportLargeObjs;
+
+    /**
+     * Whether the device supports number of changes.
+     *
+     * @var boolean
+     */
+    var $SupportNumberOfChanges;
+
+    /**
+     * Returns a SyncML_DataStore object for a certain source URI.
+     *
+     * @param string $source URI  A source URI.
+     *
+     * @return SyncML_DataStore  A data store object or null if none found for
+     *                           the source URI.
+     */
+    function getDataStore($sourceURI)
+    {
+        foreach ($this->DataStores as $dataStore) {
+            if ($dataStore->SourceRef == $sourceURI) {
+                return $dataStore;
+            }
+        }
+        return null;
+    }
+
+}
+
+/**
+ * The SyncML_DataStore class describes one of the possible datastores
+ * (i.e. databases) of the device.
+ *
+ * Most important attributes are the preferred MIME Types for sending and
+ * receiving data for this datastore: $Tx_Pref and $Rx_Pref.
+ *
+ * @package SyncML
+ */
+class SyncML_DataStore {
+
+    /**
+     * The local URI of the datastore.
+     *
+     * @var string
+     */
+    var $SourceRef;
+
+    /**
+     * The display name of the datastore
+     *
+     * @var string
+     */
+    var $DisplayName;
+
+    /**
+     * The maximum size of a global unique identifier for the datastore in
+     * bytes.
+     *
+     * @var integer
+     */
+    var $MaxGUIDSize;
+
+    /**
+     * The preferred types and versions of a content type received by the
+     * device.
+     *
+     * The content types (CTType) are the keys, the versions (VerCT) are the
+     * values.
+     *
+     * @var array
+     */
+    var $Rx_Pref = array();
+
+    /**
+     * The supported types and versions of a content type received by the
+     * device.
+     *
+     * The content types (CTType) are the keys, the versions (VerCT) are the
+     * values.
+     *
+     * @var array
+     */
+    var $Rx = array();
+
+    /**
+     * The preferred types and versions of a content type transmitted by the
+     * device.
+     *
+     * The content types (CTType) are the keys, the versions (VerCT) are the
+     * values.
+     *
+     * @var array
+     */
+    var $Tx_Pref = array();
+
+    /**
+     * The supported types and versions of a content type transmitted by the
+     * device.
+     *
+     * The content types (CTType) are the keys, the versions (VerCT) are the
+     * values.
+     *
+     * @var array
+     */
+    var $Tx = array();
+
+    /**
+     * The maximum memory and item identifier for the datastore.
+     *
+     * Not implemented yet.
+     */
+    var $DSMem;
+
+    /**
+     * The synchronization capabilities of the datastore.
+     *
+     * The synchronization types (SyncType) are stored in the keys of the
+     * hash.
+     *
+     * @var array
+     */
+    var $SyncCap = array();
+
+    /**
+     * Returns the preferred content type the client wants to receive.
+     *
+     * @return string  The device's preferred content type or null if not
+     *                 specified (which is not allowed).
+     */
+    function getPreferredRXContentType()
+    {
+        reset($this->Rx_Pref);
+        return key($this->Rx_Pref);
+    }
+
+    /**
+     * Returns the version of the preferred content type the client wants to
+     * receive.
+     *
+     * @return string  The device's preferred content type version or null if
+     *                 not specified (which is not allowed).
+     */
+    function getPreferredRXContentTypeVersion()
+    {
+        return reset($this->Rx_Pref);
+    }
+
+}
+
+/**
+ * The SyncML_Property class is used to define a single property of a data
+ * item supported by the device.
+ *
+ * The allowed contents of a property can be defined by an enumeration of
+ * valid values (ValEnum) or by a DataType/Size combination, or not at all.
+ *
+ * @package SyncML
+ */
+class SyncML_Property {
+
+    /**
+     * The supported enumerated values of the content type property.
+     *
+     * The supported values stored in the keys of the hash, e.g. 'PUBLIC' and
+     * 'PRIVATE' for a text/calendar 'CLASS' property.
+     *
+     * @var array
+     */
+    var $ValEnum;
+
+    /**
+     * The datatype of the content type property, e.g. 'chr', 'int', 'bool',
+     * etc.
+     *
+     * @var string
+     */
+    var $DataType;
+
+    /**
+     * The size of the content type property in bytes.
+     *
+     * @var integer
+     */
+    var $Size;
+
+    /**
+     * The display name of the content type property.
+     *
+     * @var string
+     */
+    var $DisplayName;
+
+    /**
+     * The supported parameters of the content type property.
+     *
+     * The parameter name (<ParamName>, e.g. 'WORK' for the text/x-vcard 'TEL'
+     * property) are the keys, SyncML_PropertyParameter objects are the
+     * values.
+     *
+     * @var array
+     */
+    var $Params;
+
+}
+
+/**
+ * The SyncML_PropertyParameter class is used to define a single parameter of
+ * a property of a data item supported by the device.
+ *
+ * The contents of a property parameter can be defined by an enumeration of
+ * valid values (ValEnum) or by a DataType/Size combination, or not at all.
+ *
+ * @package SyncML
+ */
+class SyncML_PropertyParameter {
+
+    /**
+     * The supported enumerated values of the content type property.
+     *
+     * The supported values stored in the keys of the hash, e.g. 'PUBLIC' and
+     * 'PIVATE' for a text/calendar 'CLASS' property.
+     *
+     * @var array
+     */
+    var $ValEnum;
+
+    /**
+     * The datatype of the content type property, e.g. 'chr', 'int', 'bool',
+     * etc.
+     *
+     * @var string
+     */
+    var $DataType;
+
+    /**
+     * The size of the content type property in bytes.
+     *
+     * @var integer
+     */
+    var $Size;
+
+    /**
+     * The display name of the content type property.
+     *
+     * @var string
+     */
+    var $DisplayName;
+
+}
diff --git a/framework/SyncML/SyncML/State.php b/framework/SyncML/SyncML/State.php
new file mode 100644 (file)
index 0000000..ffad437
--- /dev/null
@@ -0,0 +1,416 @@
+<?php
+
+/* Load all libraries that we need immediately or when deserializing the
+ * SyncML_State object and its property objects from the session. */
+require_once 'SyncML/DeviceInfo.php';
+require_once 'SyncML/Device.php';
+require_once 'SyncML/Constants.php';
+require_once 'SyncML/Command/SyncElement.php';
+
+/**
+ * The SyncML_State class provides a SyncML state object.
+ *
+ * $Horde: framework/SyncML/SyncML/State.php,v 1.58 2009/04/07 10:43:41 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+class SyncML_State {
+
+    /**
+     * Id of this SyncML session.
+     *
+     * This is not to confuse with the PHP session id, though it is part of
+     * the generated PHP session id.
+     *
+     * @var string
+     */
+    var $sessionID;
+
+    /**
+     * Id of the current message.
+     *
+     * @var integer
+     */
+    var $messageID;
+
+    /**
+     * The target URI as sent by the client.
+     *
+     * This is normally the URL of the RPC server. However the client is
+     * free to send anything.
+     *
+     * @var string
+     */
+    var $targetURI;
+
+    /**
+     * The source URI as sent by the client.
+     *
+     * Can be used to identify the client and is part of the PHP session id.
+     *
+     * @var string
+     */
+    var $sourceURI;
+
+    /**
+     * SyncML protocol version.
+     *
+     * 0 for SyncML 1.0, 1 for SyncML 1.1, etc.
+     *
+     * @var integer
+     */
+    var $version;
+
+    /**
+     * Username used to authenticate with the backend.
+     *
+     * @var string
+     */
+    var $user;
+
+    /**
+     * Whether this session has authenticated successfully.
+     *
+     * @var boolean
+     */
+    var $authenticated = false;
+
+    /**
+     * <SyncML> namespace uri.
+     *
+     * @var string
+     */
+    var $_uri;
+
+    /**
+     * <Meta> namespace uri.
+     *
+     * @var string
+     */
+    var $uriMeta;
+
+    /**
+     * <DevInf> namespace uri.
+     *
+     * @var string
+     */
+    var $uriDevInf;
+
+    /**
+     * Whether WBXML encoding is used.
+     *
+     * @var boolean
+     */
+    var $wbxml = false;
+
+    /**
+     * The maximum allowed message size in bytes.
+     *
+     * @todo Change to PHP_INT_MAX.
+     *
+     * @var integer
+     */
+    var $maxMsgSize = 1000000000;
+
+    /**
+     * Array of SyncML_Sync objects.
+     *
+     * @var array
+     */
+    var $_syncs = array();
+
+    /**
+     * The list of all server changes being sent to the client as a reference
+     * for Status responses from the client.
+     *
+     * @var array
+     */
+    var $serverChanges = array();
+
+    /**
+     * Name of the appropriate device driver.
+     *
+     * @var string
+     */
+    var $_deviceDriver;
+
+    /**
+     * Device info provided by the SyncML DevInf data.
+     *
+     * @var SyncML_DeviceInfo
+     */
+    var $deviceInfo;
+
+    /**
+     * Current sync element sent from client.
+     *
+     * Stored in state if one element is split into multiple message packets.
+     *
+     * @var SyncML_SyncElement
+     */
+    var $curSyncItem;
+
+    /**
+     * Flag that is set if the client sends a Final but we are not finished
+     * with the current package and thus can't final this package yet.
+     *
+     * @var boolean
+     */
+    var $delayedFinal = false;
+
+    /**
+     * Constructor.
+     */
+    function SyncML_State($sourceURI, $user, $sessionID)
+    {
+        $this->sourceURI = $sourceURI;
+        $this->user = $user;
+        $this->sessionID = $sessionID;
+
+        /* Create empty dummy device info. Will be replaced with real DevInf
+         * information if provided by the client. */
+        $this->deviceInfo = new SyncML_DeviceInfo();
+    }
+
+    /**
+     * Returns the <DevInf><VerDTD> content based on the protocol version.
+     */
+    function getVerDTD()
+    {
+        switch ($this->version) {
+            case 1:
+                return '1.1';
+            case 2:
+                return '1.2';
+            default:
+                return '1.0';
+        }
+    }
+
+    /**
+     * Returns the DevInf URI based on the protocol version.
+     */
+    function getDevInfURI()
+    {
+        switch ($this->version) {
+            case 1:
+                return './devinf11';
+            case 2:
+                return './devinf12';
+            default:
+                return './devinf10';
+        }
+    }
+
+    /**
+     * Returns the protocol name based on the protocol version.
+     */
+    function getProtocolName()
+    {
+        switch ($this->version) {
+            case 1:
+                return 'SyncML/1.1';
+            case 2:
+                return 'SyncML/1.2';
+            default:
+                return 'SyncML/1.0';
+        }
+    }
+
+    /**
+     * Sets the protocol version
+     *
+     * @param integer $version  The protocol version: 0 for SyncML 1.0, 1 for
+     *                          SyncML 1.1 etc.
+     */
+    function setVersion($version)
+    {
+        switch ($version) {
+        case 1:
+            $this->_uri = NAME_SPACE_URI_SYNCML_1_1;
+            $this->uriMeta = NAME_SPACE_URI_METINF_1_1;
+            $this->uriDevInf = NAME_SPACE_URI_DEVINF_1_1;
+            break;
+        case 2:
+            $this->_uri = NAME_SPACE_URI_SYNCML_1_2;
+            $this->uriMeta = NAME_SPACE_URI_METINF_1_2;
+            $this->uriDevInf = NAME_SPACE_URI_DEVINF_1_2;
+            break;
+        default:
+            $this->_uri = NAME_SPACE_URI_SYNCML;
+            $this->uriMeta = NAME_SPACE_URI_METINF;
+            $this->uriDevInf = NAME_SPACE_URI_DEVINF;
+            break;
+        }
+
+        $this->version = $version;
+    }
+
+    /**
+     * Returns the namespace URI for the <SyncML> element.
+     *
+     * @return string  The namespace URI to use, if any.
+     */
+    function getURI()
+    {
+        /* The non WBXML devices (notably SonyEricsson and Funambol) seem to
+         * get confused by a <SyncML xmlns="syncml:SYNCML1.1"> element. They
+         * require just <SyncML>. So don't use a namespace for non-wbxml
+         * devices. */
+        if ($this->wbxml || $this->version > 0) {
+            return $this->_uri;
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Returns a SyncML_Device instance for the device used in this session.
+     *
+     * @return SyncML_Device  A SyncML_Device instance.
+     */
+    function getDevice()
+    {
+        if (empty($this->_deviceDriver)) {
+            $si = $this->sourceURI;
+            $di = $this->deviceInfo;
+
+            if (stristr($si, 'sync4j') !== false ||
+                stristr($si, 'sc-pim') !== false ||
+                stristr($si, 'fol-') !== false ||
+                stristr($si, 'fwm-') !== false ||
+                stristr($si, 'fbb-') !== false) {
+                $this->_deviceDriver = 'Sync4j';
+            } elseif (!empty($di->Man) &&
+                      (stristr($di->Man, 'Sony Ericsson') !== false ||
+                       stristr($di->Mod, 'A1000') !== false)) {
+                /* The Morola A1000 has a similar (UIQ) firmware as the
+                 * P800: */
+                $this->_deviceDriver = 'P800';
+            } elseif (!empty($di->Man) &&
+                      stristr($di->Man, 'synthesis') !== false) {
+                $this->_deviceDriver = 'Synthesis';
+            } elseif (!empty($di->Man) &&
+                      stristr($di->Man, 'nokia') !== false) {
+                $this->_deviceDriver = 'Nokia';
+            } elseif (stristr($si, 'fmz-') !== false) {
+                $this->_deviceDriver = 'Sync4JMozilla';
+            } else {
+                $this->_deviceDriver = 'default';
+            }
+        }
+
+        return SyncML_Device::factory($this->_deviceDriver);
+    }
+
+    /**
+     * @param string $target
+     * @param SyncML_Sync $sync
+     */
+    function setSync($target, &$sync)
+    {
+        $this->_syncs[$target] = &$sync;
+    }
+
+    /**
+     * @param string $target
+     * @return SyncML_Sync
+     */
+    function &getSync($target)
+    {
+        if (isset($this->_syncs[$target])) {
+            return $this->_syncs[$target];
+        } else {
+            $sync = false;
+            return $sync;
+        }
+    }
+
+    /**
+     * @return array
+     */
+    function &getSyncs()
+    {
+        return $this->_syncs;
+    }
+
+    /**
+     * Returns whether there are any pending elements that have not been sent
+     * to due to message size restrictions. These will be sent int the next
+     * message.
+     *
+     * @return boolean  True if there are pending elements that have yet to be
+     *                  sent.
+     */
+    function hasPendingSyncs()
+    {
+        if (is_array($this->_syncs)) {
+            foreach ($this->_syncs as $sync) {
+                if ($sync->hasPendingElements()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns all syncs which have pending elements left.
+     *
+     * @return array  Array of TargetLocURIs which can be used as a key in
+     *                getSync() calls.
+     */
+    function getPendingSyncs()
+    {
+        $pending = array();
+        if (is_array($this->_syncs)) {
+            foreach ($this->_syncs as $target => $sync) {
+                if ($sync->hasPendingElements()) {
+                    $pending[] = $target;
+                }
+            }
+        }
+        return $pending;
+    }
+
+    /**
+     * Returns whether all syncs are in completed state or no syncs are
+     * present.
+     *
+     * @return boolean  True if all syncs are in completed state.
+     */
+    function isAllSyncsComplete()
+    {
+        if (is_array($this->_syncs)) {
+            foreach ($this->_syncs as $target => $sync) {
+                if (!$sync->isComplete()) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Propagates final tags here and then further to every sync.
+     *
+     * This allows the sync objects to determine if they are complete.
+     */
+    function handleFinal(&$output, $debug = false)
+    {
+        if (is_array($this->_syncs)) {
+            foreach (array_keys($this->_syncs) as $t) {
+                $this->_syncs[$t]->handleFinal($output, $debug);
+            }
+        }
+    }
+
+}
diff --git a/framework/SyncML/SyncML/Sync.php b/framework/SyncML/SyncML/Sync.php
new file mode 100644 (file)
index 0000000..99dd0e1
--- /dev/null
@@ -0,0 +1,821 @@
+<?php
+/**
+ * $Horde: framework/SyncML/SyncML/Sync.php,v 1.61 2009/08/26 10:59:22 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @since   Horde 3.0
+ * @package SyncML
+ */
+
+/** @see SyncML_Sync::_state */
+define('STATE_INIT',      0);
+define('STATE_SYNC',      1);
+define('STATE_MAP',       2);
+define('STATE_COMPLETED', 3);
+
+class SyncML_Sync {
+
+    /**
+     * Target (client) URI (database).
+     *
+     * @var string
+     */
+    var $_targetLocURI;
+
+    /**
+     * Source (server) URI (database).
+     *
+     * @var string
+     */
+    var $_sourceLocURI;
+
+    /**
+     * The synchronization method, one of the ALERT_* constants.
+     *
+     * @var integer
+     */
+    var $_syncType;
+
+    /**
+     * Counts the <Sync>s sent by the server.
+     *
+     * @var integer
+     */
+    var $_syncsSent = 0;
+
+    /**
+     * Counts the <Sync>s received by the server. Currently unused.
+     *
+     * @var integer
+     */
+    var $_syncsReceived = 0;
+
+    /**
+     * Map data is expected whenever an add is sent to the client.
+     *
+     * @var boolean
+     */
+    var $_expectingMapData = false;
+
+    /**
+     * State of the current sync.
+     *
+     * A sync starts in STATE_INIT and moves on to the next state with every
+     * <Final> received from the client: STATE_INIT, STATE_SYNC, STATE_MAP,
+     * STATE_COMPLETED.  STATE_MAP doesn't occur for _FROM_CLIENT syncs.
+     *
+     * @var constant
+     */
+    var $_state = STATE_INIT;
+
+    /**
+     * Sync Anchors determine the interval from which changes are retrieved.
+     *
+     * @var integer
+     */
+    var $_clientAnchorNext;
+
+    var $_serverAnchorLast;
+    var $_serverAnchorNext;
+
+    /**
+     * Number of objects that have been sent to the server for adding.
+     *
+     * @var integer
+     */
+    var $_client_add_count = 0;
+
+    /**
+     * Number of objects that have been sent to the server for replacement.
+     *
+     * @var integer
+     */
+    var $_client_replace_count = 0;
+
+    /**
+     * Number of objects that have been sent to the server for deletion.
+     *
+     * @var integer
+     */
+    var $_client_delete_count = 0;
+
+    /**
+     * Add due to client replace request when map entry is not found. Happens
+     * during SlowSync.
+     *
+     * @var integer
+     */
+    var $_client_addreplaces = 0;
+
+    /**
+     * Number of objects that have been sent to the client for adding.
+     *
+     * @var integer
+     */
+    var $_server_add_count = 0;
+
+    /**
+     * Number of objects that have been sent to the client for replacement.
+     *
+     * @var integer
+     */
+    var $_server_replace_count = 0;
+
+    /**
+     * Number of objects that have been sent to the client for deletion.
+     *
+     * @var integer
+     */
+    var $_server_delete_count = 0;
+
+    /**
+     * Number of failed actions, for logging purposes only.
+     *
+     * @var integer
+     */
+    var $_errors = 0;
+
+    /**
+     * List of object UIDs (in the keys) that have been added on the server
+     * since the last synchronization and are supposed to be sent to the
+     * client.
+     *
+     * @var array
+     */
+    var $_server_adds;
+
+    /**
+     * List of object UIDs (in the keys) that have been changed on the server
+     * since the last synchronization and are supposed to be sent to the
+     * client.
+     *
+     * @var array
+     */
+    var $_server_replaces;
+
+    /**
+     * List of object UIDs (in the keys) that have been deleted on the server
+     * since the last synchronization and are supposed to be sent to the
+     * client.
+     *
+     * @var array
+     */
+    var $_server_deletes;
+
+    /**
+     * List of task UIDs (in the keys) that have been added on the server
+     * since the last synchronization and are supposed to be sent to the
+     * client.
+     *
+     * This is only used for clients handling tasks and events in one
+     * database. We need to seperately store the server tasks adds, so when we
+     * get a Map command from the client, we know whether to put this in tasks
+     * or calendar.
+     *
+     * @var array
+     */
+    var $_server_task_adds;
+
+    function SyncML_Sync($syncType, $serverURI, $clientURI, $serverAnchorLast,
+                         $serverAnchorNext, $clientAnchorNext)
+    {
+        $this->_syncType = $syncType;
+        $this->_targetLocURI = $serverURI;
+        $this->_sourceLocURI = $clientURI;
+        $this->_clientAnchorNext = $clientAnchorNext;
+        $this->_serverAnchorLast = $serverAnchorLast;
+        $this->_serverAnchorNext = $serverAnchorNext;
+    }
+
+    /**
+     * Here's where the actual processing of a client-sent Sync Item takes
+     * place. Entries are added, deleted or replaced from the server database
+     * by using backend API calls.
+     *
+     * @todo maybe this should be moved to SyncItem
+     *
+     * @param $output
+     * @param SyncML_SyncElement $item
+     */
+    function handleClientSyncItem(&$output, &$item)
+    {
+        global $backend;
+
+        $backend->logMessage(
+            'Handling <' . $item->elementType . '> sent from client',
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* if size of item is set: check it first */
+        if ($item->size > 0) {
+            if (strlen($item->content) != $item->size &&
+                /* For some strange reason the SyncML conformance test suite
+                 * sends an item with length n and size tag=n+1 and expects us
+                 * the accept it. Happens in test 1301.  So ignore this to be
+                 * conformant (and wrong). */
+                strlen($item->content) + 1 != $item->size) {
+                $item->responseCode = RESPONSE_SIZE_MISMATCH;
+                $backend->logMessage(
+                    'Item size mismatch. Size reported as ' . $item->size
+                    . ' but actual size is ' . strlen($item->content),
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+                $this->_errors++;
+                return false;
+            }
+        }
+
+        $device = $_SESSION['SyncML.state']->getDevice();
+        $hordedatabase = $database = $this->_targetLocURI;
+        $content = $item->content;
+        if ($item->contentFormat == 'b64') {
+            $content = base64_decode($content);
+        }
+
+        if (($item->contentType == 'text/calendar' ||
+             $item->contentType == 'text/x-vcalendar') &&
+            $backend->_normalize($database) == 'calendar' && 
+            $device->handleTasksInCalendar()) {
+            $tasksincalendar = true;
+            /* Check if the client sends us a vtodo in a calendar sync. */
+            if (preg_match('/(\r\n|\r|\n)BEGIN[^:]*:VTODO/',
+                           "\n" . $content)) {
+                $hordedatabase = $this->_taskToCalendar($backend->_normalize($database));
+             }
+        } else {
+            $tasksincalendar = false;
+        }
+
+        /* Use contentType explicitly specified in this sync command. */
+        $contentType = $item->contentType;
+
+        /* If not provided, use default from device info. */
+        if (!$contentType) {
+            $contentType = $device->getPreferredContentType($hordedatabase);
+        }
+
+        if ($item->elementType != 'Delete') {
+            list($content, $contentType) = $device->convertClient2Server($content, $contentType);
+        }
+
+        $cuid = $item->cuid;
+        $suid = false;
+
+        if ($item->elementType == 'Add') {
+            /* Handle client add requests.
+             *
+             * @todo: check if this $cuid is already present and then maybe do
+             * an replace instead? */
+            $suid = $backend->addEntry($hordedatabase, $content, $contentType, $cuid);
+            if (!is_a($suid, 'PEAR_Error')) {
+                $this->_client_add_count++;
+                $item->responseCode = RESPONSE_ITEM_ADDED;
+                $backend->logMessage('Added client entry as ' . $suid,
+                                     __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            } else {
+                $this->_errors++;
+                /* @todo: better response code. */
+                $item->responseCode = RESPONSE_NO_EXECUTED;
+                $backend->logMessage('Error in adding client entry: ' . $suid->message, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        } elseif ($item->elementType == 'Delete') {
+            /* Handle client delete requests. */
+            $ok = $backend->deleteEntry($database, $cuid);
+            if (!$ok && $tasksincalendar) {
+                $backend->logMessage(
+                    'Task ' . $cuid . ' deletion sent with calendar request',
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $ok = $backend->deleteEntry($this->_taskToCalendar($backend->_normalize($database)), $cuid);
+            }
+
+            if ($ok) {
+                $this->_client_delete_count++;
+                $item->responseCode = RESPONSE_OK;
+                $backend->logMessage('Deleted entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            } else {
+                $this->_errors++;
+                $item->responseCode = RESPONSE_ITEM_NO_DELETED;
+                $backend->logMessage('Failure deleting client entry, maybe already disappeared from server', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            }
+
+        } elseif ($item->elementType == 'Replace') {
+            /* Handle client replace requests. */
+            $suid = $backend->replaceEntry($hordedatabase, $content,
+                                           $contentType, $cuid);
+
+            if (!is_a($suid, 'PEAR_Error')) {
+                $this->_client_replace_count++;
+                $item->responseCode = RESPONSE_OK;
+                $backend->logMessage('Replaced entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            } else {
+                $backend->logMessage($suid->message, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                /* Entry may have been deleted; try adding it. */
+                $suid = $backend->addEntry($hordedatabase, $content,
+                                           $contentType, $cuid);
+                if (!is_a($suid, 'PEAR_Error')) {
+                    $this->_client_addreplaces++;
+                    $item->responseCode = RESPONSE_ITEM_ADDED;
+                    $backend->logMessage(
+                        'Added instead of replaced entry ' . $suid,
+                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                } else {
+                    $this->_errors++;
+                    /* @todo: better response code. */
+                    $item->responseCode = RESPONSE_NO_EXECUTED;
+                    $backend->logMessage(
+                        'Error in adding client entry due to replace request: '
+                        . $suid->message,
+                        __FILE__, __LINE__, PEAR_LOG_ERR);
+                }
+            }
+        } else {
+            $backend->logMessage(
+                'Unexpected elementType: ' . $item->elementType,
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        return $suid;
+    }
+
+    /**
+     * Creates a <Sync> output containing the server changes.
+     *
+     * @todo Check for Mem/FreeMem and Mem/FreeID when checking MaxObjSize
+     */
+    function createSyncOutput(&$output)
+    {
+        global $backend, $messageFull;
+
+        $backend->logMessage(
+            'Creating <Sync> output for server changes in database '
+            . $this->_targetLocURI,
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* If sync data from client only, nothing to be done here. */
+        if($this->_syncType == ALERT_ONE_WAY_FROM_CLIENT ||
+           $this->_syncType == ALERT_REFRESH_FROM_CLIENT) {
+            return;
+        }
+
+        /* If one sync has been sent an no pending data: bail out. */
+        if ($this->_syncsSent > 0 && !$this->hasPendingElements()) {
+            return;
+        }
+
+        /* $messageFull will be set to true to indicate that there's no room
+         * for other data in this message. If it's false (empty) and there are
+         * pending Sync data, the final command will sent the pending data. */
+        $messageFull = false;
+
+        $state = &$_SESSION['SyncML.state'];
+        $device = $state->getDevice();
+        $contentType = $device->getPreferredContentTypeClient(
+            $this->_targetLocURI, $this->_sourceLocURI);
+        $contentTypeTasks = $device->getPreferredContentTypeClient(
+            'tasks', $this->_sourceLocURI);
+
+        /* If server modifications are not retrieved yet (first Sync element),
+         * do it now. */
+        if (!is_array($this->_server_adds)) {
+            $backend->logMessage(
+                'Compiling server changes from '
+                . date('Y-m-d H:i:s', $this->_serverAnchorLast)
+                . ' to ' . date('Y-m-d H:i:s', $this->_serverAnchorNext),
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $result = $this->_retrieveChanges($this->_targetLocURI,
+                                              $this->_server_adds,
+                                              $this->_server_replaces,
+                                              $this->_server_deletes);
+            if (is_a($result, 'PEAR_Error')) {
+                return;
+            }
+
+            /* If tasks are handled inside calendar, do the same again for
+             * tasks. Merge resulting arrays. */
+            if ($backend->_normalize($this->_targetLocURI) == 'calendar' && 
+                $device->handleTasksInCalendar()) {
+                $backend->logMessage('Handling tasks in calendar sync',
+                                     __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                $this->_server_task_adds = $deletes2 = $replaces2 = array();
+                $result = $this->_retrieveChanges('tasks',
+                                                  $this->_server_task_adds,
+                                                  $replaces2,
+                                                  $deletes2);
+                if (is_a($result, 'PEAR_Error')) {
+                    return;
+                }
+                $this->_server_adds = array_merge($this->_server_adds,
+                                                  $this->_server_task_adds);
+                $this->_server_replaces = array_merge($this->_server_replaces,
+                                                      $replaces2);
+                $this->_server_deletes = array_merge($this->_server_deletes,
+                                                     $deletes2);
+            }
+
+            $numChanges = count($this->_server_adds)
+                + count($this->_server_replaces)
+                + count($this->_server_deletes);
+            $backend->logMessage(
+                'Sending ' . $numChanges . ' server changes ' . 'for client URI '
+                . $this->_targetLocURI,
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            /* Now we know the number of Changes and can send them to the
+             * client. */
+            $di = $state->deviceInfo;
+            if ($di->SupportNumberOfChanges) {
+                $output->outputSyncStart($this->_sourceLocURI,
+                                         $this->_targetLocURI,
+                                         $numChanges);
+            } else {
+                $output->outputSyncStart($this->_sourceLocURI,
+                                         $this->_targetLocURI);
+            }
+        } else {
+            /* Package continued. Sync in subsequent message. */
+            $output->outputSyncStart($this->_sourceLocURI,
+                                     $this->_targetLocURI);
+        }
+
+        /* We sent a Sync. So at least we espect a status response and thus
+         * another message from the client. */
+        $GLOBALS['message_expectresponse'] = true;
+
+        /* Handle deletions. */
+        $deletes = $this->_server_deletes;
+        foreach ($deletes as $suid => $cuid) {
+            /* Check if we have space left in the message. */
+            if ($state->maxMsgSize - $output->getOutputSize() < MSG_TRAILER_LEN) {
+                $backend->logMessage(
+                    'Maximum message size ' . $state->maxMsgSize
+                    . ' approached during delete; current size: '
+                    . $output->getOutputSize(),
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $messageFull = true;
+                $output->outputSyncEnd();
+                $this->_syncsSent += 1;
+                return;
+            }
+            $backend->logMessage(
+                "Sending delete from server: client id $cuid, server id $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            /* Create a Delete request for client. */
+            $cmdId = $output->outputSyncCommand('Delete', null, null, null, $cuid, null);
+            unset($this->_server_deletes[$suid]);
+            $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, $cuid);
+            $this->_server_delete_count++;
+        }
+
+        /* Handle additions. */
+        $adds = $this->_server_adds;
+        foreach ($adds as $suid => $cuid) {
+            $backend->logMessage("Sending add from server: $suid",
+                                 __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $syncDB = isset($this->_server_task_adds[$suid]) ? 'tasks' : $this->_targetLocURI;
+            $ct = isset($this->_server_task_adds[$suid]) ? $contentTypeTasks : $contentType;
+
+            $c = $backend->retrieveEntry($syncDB, $suid, $ct);
+            /* Item in history but not in database. Strange, but can
+             * happen. */
+            if (is_a($c, 'PEAR_Error')) {
+                $backend->logMessage(
+                    'API export call for ' . $suid . ' failed: '
+                    . $c->getMessage(),
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                list($clientContent, $clientContentType, $clientEncodingType) =
+                    $device->convertServer2Client($c, $contentType, $syncDB);
+                /* Check if we have space left in the message. */
+                if (($state->maxMsgSize - $output->getOutputSize() - strlen($clientContent)) < MSG_TRAILER_LEN) {
+                    $backend->logMessage(
+                        'Maximum message size ' . $state->maxMsgSize
+                        . ' approached during add; current size: '
+                        . $output->getOutputSize(),
+                        __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                    if (strlen($clientContent) + MSG_DEFAULT_LEN > $state->maxMsgSize) {
+                        $backend->logMessage(
+                            'Data item won\'t fit into a single message. Partial sending not implemented yet. Item will not be sent!',
+                            __FILE__, __LINE__, PEAR_LOG_WARNING);
+                        /* @todo: implement partial sending instead of
+                         * dropping item! */
+                        unset($this->_server_adds[$suid]);
+                        continue;
+                    }
+                    $messageFull = true;
+                    $output->outputSyncEnd();
+                    $this->_syncsSent += 1;
+                    return;
+                }
+
+                /* @todo: on SlowSync send Replace instead! */
+                // $output->outputSyncCommand($refts == 0 ? 'Replace' : 'Add',
+                $cmdId = $output->outputSyncCommand('Add', $clientContent,
+                                                    $clientContentType,
+                                                    $clientEncodingType,
+                                                    null, $suid);
+                $this->_server_add_count++;
+            }
+            unset($this->_server_adds[$suid]);
+            $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, 0);
+        }
+
+        if ($this->_server_add_count) {
+            $this->_expectingMapData = true;
+        }
+
+        /* Handle Replaces. */
+        $replaces = $this->_server_replaces;
+        foreach ($replaces as $suid => $cuid) {
+            $syncDB = isset($replaces2[$suid]) ? 'tasks' : $this->_targetLocURI;
+            $ct = isset($replaces2[$suid]) ? $contentTypeTasks : $contentType;
+            $c = $backend->retrieveEntry($syncDB, $suid, $ct);
+            if (is_a($c, 'PEAR_Error')) {
+                /* Item in history but not in database. */
+                unset($this->_server_replaces[$suid]);
+                continue;
+            }
+
+            $backend->logMessage(
+                "Sending replace from server: $suid",
+                __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            list($clientContent, $clientContentType, $clientEncodingType) =
+                $device->convertServer2Client($c, $contentType, $syncDB);
+            /* Check if we have space left in the message. */
+            if (($state->maxMsgSize - $output->getOutputSize() - strlen($clientContent)) < MSG_TRAILER_LEN) {
+                $backend->logMessage(
+                    'Maximum message size ' . $state->maxMsgSize
+                    . ' approached during replace; current size: '
+                    . $output->getOutputSize(),
+                    __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                if (strlen($clientContent) + MSG_DEFAULT_LEN > $state->maxMsgSize) {
+                    $backend->logMessage(
+                        'Data item won\'t fit into a single message. Partial sending not implemented yet. Item will not be sent!',
+                        __FILE__, __LINE__, PEAR_LOG_WARNING);
+                    /* @todo: implement partial sending instead of
+                     * dropping item! */
+                    unset($this->_server_replaces[$suid]);
+                    continue;
+                }
+                $messageFull = true;
+                $output->outputSyncEnd();
+                $this->_syncsSent += 1;
+                return;
+            }
+            $cmdId = $output->outputSyncCommand('Replace', $clientContent,
+                                                $clientContentType,
+                                                $clientEncodingType,
+                                                $cuid, null);
+            $this->_server_replace_count++;
+            unset($this->_server_replaces[$suid]);
+            $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, $cuid);
+        }
+
+        /* Finished! Send closing </Sync>. */
+        $output->outputSyncEnd();
+        $this->_syncsSent += 1;
+    }
+
+    /**
+     * Retrieves and condenses the changes on the server side since the last
+     * synchronization.
+     *
+     * @param string $syncDB   The database being synchronized.
+     * @param array $adds      Will be set with the server-client-uid mappings
+     *                         of added objects.
+     * @param array $replaces  Will be set with the server-client-uid mappings
+     *                         of changed objects.
+     * @param array $deletes   Will be set with the server-client-uid mappings
+     *                         of deleted objects.
+     */
+    function _retrieveChanges($syncDB, &$adds, &$replaces, &$deletes)
+    {
+        $adds = $replaces = $deletes = array();
+        $result = $GLOBALS['backend']->getServerChanges($syncDB,
+                                                        $this->_serverAnchorLast,
+                                                        $this->_serverAnchorNext,
+                                                        $adds, $replaces, $deletes);
+        if (is_a($result, 'PEAR_Error')) {
+            $GLOBALS['backend']->logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+    }
+
+    /**
+     * Notifies the sync that a final has been received by the client.
+     *
+     * Depending on the current state of the sync this can mean various
+     * things:
+     * a) Init phase (Alerts) done. Next package contaings actual syncs.
+     * b) Sync sending from client done. Next package are maps (or finish
+     *    or finish if ONE_WAY_FROM_CLIENT sync
+     * c) Maps finished, completly done.
+     */
+    function handleFinal(&$output, $debug = false)
+    {
+        switch ($this->_state) {
+        case STATE_INIT:
+            $state = 'Init';
+            break;
+        case STATE_SYNC:
+            $state = 'Sync';
+            break;
+        case STATE_MAP:
+            $state = 'Map';
+            break;
+        case STATE_COMPLETED:
+            $state = 'Completed';
+            break;
+        }
+
+        $GLOBALS['backend']->logMessage('Handle <Final> for state ' . $state,
+                                         __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        switch ($this->_state) {
+        case STATE_INIT:
+            $this->_state = STATE_SYNC;
+            break;
+        case STATE_SYNC:
+            /* Received all client Sync data, now we are allowed to send
+             * server sync data. */
+            if (!$debug) {
+                $this->createSyncOutput($output);
+            }
+
+            // FROM_CLIENT_SYNC doeesn't require a MAP package:
+            if ($this->_syncType == ALERT_ONE_WAY_FROM_CLIENT ||
+                $this->_syncType == ALERT_REFRESH_FROM_CLIENT ||
+                !$this->_expectingMapData) {
+                $this->_state = STATE_COMPLETED;
+            } else {
+                $this->_state = STATE_MAP;
+            }
+            break;
+        case STATE_MAP:
+            $this->_state = STATE_COMPLETED;
+            break;
+        }
+    }
+
+    /**
+     * Returns true if there are still outstanding server sync items to
+     * be sent to the client.
+     *
+     * This is the case if the MaxMsgSize has been reached and the pending
+     * elements are to be sent in another message.
+     */
+    function hasPendingElements()
+    {
+        if (!is_array($this->_server_adds)) {
+            /* Changes not compiled yet: not pending: */
+            return false;
+        }
+
+        return (count($this->_server_adds) + count($this->_server_replaces) + count($this->_server_deletes)) > 0;
+    }
+
+    function addSyncReceived()
+    {
+        $this->_syncsReceived++;
+    }
+
+    /* Currently unused */
+    function getSyncsReceived()
+    {
+        return $this->_syncsReceived;
+    }
+
+    function isComplete()
+    {
+        return $this->_state == STATE_COMPLETED;
+    }
+
+    /**
+     * Completes a sync once everything is done: store the sync anchors so the
+     * next sync can be a delta sync and produce some debug info.
+     */
+    function closeSync()
+    {
+        $GLOBALS['backend']->writeSyncAnchors($this->_targetLocURI,
+                                              $this->_clientAnchorNext,
+                                              $this->_serverAnchorNext);
+
+        $s = sprintf(
+            'Finished sync of database %s. Failures: %d; '
+            . 'changes from client (Add, Replace, Delete, AddReplaces): %d, %d, %d, %d; '
+            . 'changes from server (Add, Replace, Delete): %d, %d, %d',
+            $this->_targetLocURI,
+            $this->_errors,
+            $this->_client_add_count,
+            $this->_client_replace_count,
+            $this->_client_delete_count,
+            $this->_client_addreplaces,
+            $this->_server_add_count,
+            $this->_server_replace_count,
+            $this->_server_delete_count);
+        $GLOBALS['backend']->logMessage($s , __FILE__, __LINE__, PEAR_LOG_INFO);
+    }
+
+    function getServerLocURI()
+    {
+        return $this->_targetLocURI;
+    }
+
+    function getClientLocURI()
+    {
+        return $this->_sourceLocURI;
+    }
+
+    function getClientAnchorNext()
+    {
+        return $this->_clientAnchorNext;
+    }
+
+    function getServerAnchorNext()
+    {
+        return $this->_serverAnchorNext;
+    }
+
+    function getServerAnchorLast()
+    {
+        return $this->_serverAnchorLast;
+    }
+
+    function createUidMap($databaseURI, $cuid, $suid)
+    {
+        $device = $_SESSION['SyncML.state']->getDevice();
+
+        if ($GLOBALS['backend']->_normalize($databaseURI) == 'calendar' &&
+            $device->handleTasksInCalendar() &&
+            isset($this->_server_task_adds[$suid])) {
+            $db = $this->_taskToCalendar($GLOBALS['backend']->_normalize($databaseURI));
+        } else {
+            $db = $databaseURI;
+        }
+
+        $GLOBALS['backend']->createUidMap($db, $cuid, $suid);
+        $GLOBALS['backend']->logMessage(
+            'Created map for client id ' . $cuid . ' and server id ' . $suid
+            . ' in database ' . $db,
+            __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+    }
+
+    /**
+     * Returns the client ID of server change identified by the change type
+     * and server ID.
+     *
+     * @param string $change  The change type (add, replace, delete).
+     * @param string $id      The object's server UID.
+     *
+     * @return string  The matching client ID or null if none found.
+     */
+    function getServerChange($change, $id)
+    {
+        $property = '_server_' . $change . 's';
+        return isset($this->$property[$id]) ? $this->$property[$id] : null;
+    }
+
+    /**
+     * Sets the client ID of server change identified by the change type and
+     * server ID.
+     *
+     * @param string $change  The change type (add, replace, delete).
+     * @param string $sid     The object's server UID.
+     * @param string $cid     The object's client UID.
+     */
+    function setServerChange($change, $sid, $cid)
+    {
+        $property = '_server_' . $change . 's';
+        $this->$property[$sid] = $cid;
+    }
+
+    /**
+     * Unsets the server-client-map of server change identified by the change
+     * type and server ID.
+     *
+     * @param string $change  The change type (add, replace, delete).
+     * @param string $id      The object's server UID.
+     */
+    function unsetServerChange($change, $id)
+    {
+        $property = '_server_' . $change . 's';
+        unset($this->$property[$id]);
+    }
+
+    /**
+     * Converts a calendar databaseURI to a tasks databaseURI for devices with
+     * handleTasksInCalendar.
+     */
+    function _taskToCalendar($databaseURI)
+    {
+        return str_replace('calendar', 'tasks', $databaseURI);
+    }
+
+}
diff --git a/framework/SyncML/SyncML/XMLOutput.php b/framework/SyncML/SyncML/XMLOutput.php
new file mode 100644 (file)
index 0000000..92bffe9
--- /dev/null
@@ -0,0 +1,658 @@
+<?php
+/**
+ * This class creates the actual XML data and passes it on to a ContentHandler
+ * for optional WBXML encoding.
+ *
+ * Each member function creates one type of SyncML artefact (like a Status
+ * response).  Currently some of the information is retrieved from
+ * state. Maybe remove these dependencies (by providing the data as parameter)
+ * for an even cleaner implementation.
+ *
+ * The SyncML_XMLOutput class takes automatically care of creating a unique
+ * CmdID for each command created.
+ *
+ * $Horde: framework/SyncML/SyncML/XMLOutput.php,v 1.22 2009/01/06 17:49:48 jan Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @since   Horde 3.2
+ * @package SyncML
+ */
+
+class SyncML_XMLOutput {
+
+    /**
+     * The CmdID provides a unique ID for each command in a syncml packet.
+     */
+    var $_msg_CmdID;
+
+    /**
+     *  The outputhandler to whom the XML is passed: like  XML_WBXML_Encoder
+     */
+    var $_output;
+
+    var $_uri;
+
+    /**
+     * The final output as procuded by the _output Encoder. Either an
+     * XML string or a WBXML string.
+     */
+    function getOutput()
+    {
+        return $this->_output->getOutput();
+    }
+
+    /**
+     * The length of the output as produced by the Encoder. To limit the
+     * size of individual messages.
+     */
+    function getOutputSize()
+    {
+        return $this->_output->getOutputSize();
+    }
+
+    /**
+     * To we create wbxml or not?
+     */
+    function isWBXML()
+    {
+        return is_a($this->_output, 'XML_WBXML_Encoder');
+    }
+
+    function &singleton()
+    {
+        static $instance;
+        if (!isset($instance)) {
+            $instance = new SyncML_XMLOutput();
+        }
+        return $instance;
+    }
+
+    function init(&$theoutputhandler)
+    {
+        $this->_output = $theoutputhandler;
+        $this->_msg_CmdID = 1;
+
+    }
+
+    /**
+     * Creates a SyncHdr output.
+     *
+     * Required data is retrieved from state.
+     *
+     * @param string $respURI  The url of the server endpoint.
+     */
+    function outputHeader($respURI)
+    {
+        $state = &$_SESSION['SyncML.state'];
+        $this->_uriMeta = $state->uriMeta;
+
+        $this->_output->startElement($this->_uri, 'SyncHdr');
+
+        $this->_output->startElement($this->_uri, 'VerDTD');
+        $chars = $state->getVerDTD();
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'VerDTD');
+
+        $this->_output->startElement($this->_uri, 'VerProto');
+        $chars = $state->getProtocolName();
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'VerProto');
+
+        $this->_output->startElement($this->_uri, 'SessionID');
+        $this->_output->characters($state->sessionID);
+        $this->_output->endElement($this->_uri, 'SessionID');
+
+        $this->_output->startElement($this->_uri, 'MsgID');
+        $this->_output->characters($state->messageID);
+        $this->_output->endElement($this->_uri, 'MsgID');
+
+        $this->_output->startElement($this->_uri, 'Target');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        // Source URI sent from client is Target for the server
+        $this->_output->characters($state->sourceURI);
+        $this->_output->endElement($this->_uri, 'LocURI');
+        if ($state->user) {
+            $this->_output->startElement($this->_uri, 'LocName');
+            $this->_output->characters($state->user);
+            $this->_output->endElement($this->_uri, 'LocName');
+        }
+        $this->_output->endElement($this->_uri, 'Target');
+
+        $this->_output->startElement($this->_uri, 'Source');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        // Target URI sent from client is Source for the server
+        $this->_output->characters($state->targetURI);
+        $this->_output->endElement($this->_uri, 'LocURI');
+        $this->_output->endElement($this->_uri, 'Source');
+
+        if ($respURI) {
+            $this->_output->startElement($this->_uri, 'RespURI');
+            $this->_output->characters($respURI);
+            $this->_output->endElement($this->_uri, 'RespURI');
+        }
+
+        // @Todo: omit this in SyncML1.0?
+        $this->_output->startElement($this->_uri, 'Meta');
+
+        // Dummy Max MsqSize, this is just put in to make the packet
+        // work, it is not a real value.
+        $this->_output->startElement($this->_uriMeta, 'MaxMsgSize');
+        $chars = SERVER_MAXMSGSIZE; // 1Meg
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uriMeta, 'MaxMsgSize');
+
+
+        // MaxObjSize, required by protocol for SyncML1.1 and higher.
+        if ($state->version > 0) {
+            $this->_output->startElement($this->_uriMeta, 'MaxObjSize');
+            $this->_output->characters(SERVER_MAXOBJSIZE);
+            $this->_output->endElement($this->_uriMeta, 'MaxObjSize');
+        }
+        $this->_output->endElement($this->_uri, 'Meta');
+
+        $this->_output->endElement($this->_uri, 'SyncHdr');
+    }
+
+    function outputInit()
+    {
+        $this->_uri = $_SESSION['SyncML.state']->getURI();
+
+        $this->_output->startElement($this->_uri, 'SyncML', array());
+    }
+
+    function outputBodyStart()
+    {
+        $this->_output->startElement($this->_uri, 'SyncBody', array());
+    }
+
+    function outputFinal()
+    {
+        $this->_output->startElement($this->_uri, 'Final', array());
+        $this->_output->endElement($this->_uri, 'Final');
+    }
+
+    function outputEnd()
+    {
+        $this->_output->endElement($this->_uri, 'SyncBody', array());
+        $this->_output->endElement($this->_uri, 'SyncML', array());
+    }
+
+
+    function outputStatus($cmdRef, $cmd, $data,
+                         $targetRef = '', $sourceRef = '',
+                         $syncAnchorNext = '',
+                         $syncAnchorLast = '')
+    {
+        $state = &$_SESSION['SyncML.state'];
+        $uriMeta = $state->uriMeta;
+
+        $this->_output->startElement($this->_uri, 'Status');
+        $this->_outputCmdID();
+
+        $this->_output->startElement($this->_uri, 'MsgRef');
+        $chars = $state->messageID;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'MsgRef');
+
+        $this->_output->startElement($this->_uri, 'CmdRef');
+        $chars = $cmdRef;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'CmdRef');
+
+        $this->_output->startElement($this->_uri, 'Cmd');
+        $chars = $cmd;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'Cmd');
+
+        if (!empty($targetRef)) {
+            $this->_output->startElement($this->_uri, 'TargetRef');
+            $this->_output->characters($targetRef);
+            $this->_output->endElement($this->_uri, 'TargetRef');
+        }
+
+        if (!empty($sourceRef)) {
+            $this->_output->startElement($this->_uri, 'SourceRef');
+            $this->_output->characters($sourceRef);
+            $this->_output->endElement($this->_uri, 'SourceRef');
+        }
+
+        // If we are responding to the SyncHdr and we are not
+        // authenticated then request basic authorization.
+        if ($cmd == 'SyncHdr' && !$state->authenticated) {
+            // Keep RESPONSE_CREDENTIALS_MISSING, otherwise set to
+            // RESPONSE_INVALID_CREDENTIALS.
+            $data = $data == RESPONSE_CREDENTIALS_MISSING
+                ? RESPONSE_CREDENTIALS_MISSING
+                : RESPONSE_INVALID_CREDENTIALS;
+
+            $this->_output->startElement($this->_uri, 'Chal');
+            $this->_output->startElement($this->_uri, 'Meta');
+
+            $this->_output->startElement($uriMeta, 'Type');
+            $this->_output->characters('syncml:auth-basic');
+            $this->_output->endElement($uriMeta, 'Type');
+
+            $this->_output->startElement($uriMeta, 'Format');
+            $this->_output->characters('b64');
+            $this->_output->endElement($uriMeta, 'Format');
+
+            $this->_output->endElement($this->_uri, 'Meta');
+            $this->_output->endElement($this->_uri, 'Chal');
+
+        }
+
+        $this->_output->startElement($this->_uri, 'Data');
+        $this->_output->characters($data);
+        $this->_output->endElement($this->_uri, 'Data');
+
+        if (!empty($syncAnchorNext) || !empty($syncAnchorNLast)) {
+            $this->_output->startElement($this->_uri, 'Item');
+            $this->_output->startElement($this->_uri, 'Data');
+
+            $this->_output->startElement($uriMeta, 'Anchor');
+
+            if (!empty($syncAnchorLast)) {
+              $this->_output->startElement($uriMeta, 'Last');
+              $this->_output->characters($syncAnchorLast);
+              $this->_output->endElement($uriMeta, 'Last');
+            }
+
+            if (!empty($syncAnchorNext)) {
+              $this->_output->startElement($uriMeta, 'Next');
+              $this->_output->characters($syncAnchorNext);
+              $this->_output->endElement($uriMeta, 'Next');
+            }
+
+            $this->_output->endElement($uriMeta, 'Anchor');
+
+            $this->_output->endElement($this->_uri, 'Data');
+            $this->_output->endElement($this->_uri, 'Item');
+        }
+
+        $this->_output->endElement($this->_uri, 'Status');
+
+    }
+
+    function outputDevInf($cmdRef)
+    {
+        $state = &$_SESSION['SyncML.state'];
+        $uriMeta = $state->uriMeta;
+        $uriDevInf = $state->uriDevInf;
+
+        $this->_output->startElement($this->_uri, 'Results');
+        $this->_outputCmdID();
+
+        $this->_output->startElement($this->_uri, 'MsgRef');
+        $chars = $state->messageID;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'MsgRef');
+
+        $this->_output->startElement($this->_uri, 'CmdRef');
+        $chars = $cmdRef;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'CmdRef');
+
+        $this->_output->startElement($this->_uri, 'Meta');
+        $this->_output->startElement($uriMeta, 'Type');
+        if ($state->wbxml) {
+            $this->_output->characters(MIME_SYNCML_DEVICE_INFO_WBXML);
+        } else {
+            $this->_output->characters(MIME_SYNCML_DEVICE_INFO_XML);
+        }
+
+        $this->_output->endElement($uriMeta, 'Type');
+        $this->_output->endElement($this->_uri, 'Meta');
+
+        $this->_output->startElement($this->_uri, 'Item');
+        $this->_output->startElement($this->_uri, 'Source');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        $this->_output->characters($state->getDevInfURI());
+        $this->_output->endElement($this->_uri, 'LocURI');
+        $this->_output->endElement($this->_uri, 'Source');
+
+        $this->_output->startElement($this->_uri, 'Data');
+
+        /* DevInf data is stored in wbxml not as a seperate codepage but
+         * rather as a complete wbxml stream as opaque data.  So we need a
+         * new Handler. */
+        $devinfoutput = $this->_output->createSubHandler();
+
+        $devinfoutput->startElement($uriDevInf , 'DevInf');
+        $devinfoutput->startElement($uriDevInf , 'VerDTD');
+        $devinfoutput->characters($state->getVerDTD());
+        $devinfoutput->endElement($uriDevInf , 'VerDTD');
+        $devinfoutput->startElement($uriDevInf , 'Man');
+        $devinfoutput->characters('The Horde Project (http://www.horde.org/)');
+        $devinfoutput->endElement($uriDevInf , 'Man');
+        $devinfoutput->startElement($uriDevInf , 'DevID');
+        $devinfoutput->characters(isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
+        $devinfoutput->endElement($uriDevInf , 'DevID');
+        $devinfoutput->startElement($uriDevInf , 'DevTyp');
+        $devinfoutput->characters('server');
+        $devinfoutput->endElement($uriDevInf , 'DevTyp');
+
+        if ($state->version > 0) {
+            $devinfoutput->startElement($uriDevInf , 'SupportLargeObjs');
+            $devinfoutput->endElement($uriDevInf , 'SupportLargeObjs');
+
+            $devinfoutput->startElement($uriDevInf , 'SupportNumberOfChanges');
+            $devinfoutput->endElement($uriDevInf , 'SupportNumberOfChanges');
+        }
+        $this->_writeDataStore('notes', 'text/plain', '1.0', $devinfoutput);
+        $this->_writeDataStore('contacts', 'text/directory', '3.0',
+                               $devinfoutput, array('text/x-vcard' => '2.1'));
+        $this->_writeDataStore('tasks', 'text/calendar', '2.0', $devinfoutput,
+                               array('text/x-vcalendar' => '1.0'));
+        $this->_writeDataStore('calendar', 'text/calendar', '2.0',
+                               $devinfoutput,
+                               array('text/x-vcalendar' => '1.0'));
+        $devinfoutput->endElement($uriDevInf , 'DevInf');
+
+        $this->_output->opaque($devinfoutput->getOutput());
+        $this->_output->endElement($this->_uri, 'Data');
+        $this->_output->endElement($this->_uri, 'Item');
+        $this->_output->endElement($this->_uri, 'Results');
+    }
+
+    /**
+     * Writes DevInf data for one DataStore.
+     *
+     * @param string $sourceref                 Data for <SourceRef> element.
+     * @param string $mimetype                  Data for <Rx-Pref><CTType> and
+     *                                          <Tx-Pref><CTType>.
+     * @param string $version                   Data for <Rx-Pref><VerCT> and
+     *                                          <Tx-Pref><VerCT>.
+     * @param XML_WBXML_ContentHandler $output  Content handler that will
+     *                                          received the output.
+     * @param array $additionaltypes            Array of additional types for
+     *                                          <Tx> and <Rx>; format
+     *                                          array('text/directory' => '3.0')
+     */
+    function _writeDataStore($sourceref, $mimetype, $version, &$output,
+                             $additionaltypes = array())
+    {
+        $uriDevInf = $_SESSION['SyncML.state']->uriDevInf;
+
+        $output->startElement($uriDevInf , 'DataStore');
+        $output->startElement($uriDevInf , 'SourceRef');
+        $output->characters($sourceref);
+        $output->endElement($uriDevInf , 'SourceRef');
+
+        $output->startElement($uriDevInf , 'Rx-Pref');
+        $output->startElement($uriDevInf , 'CTType');
+        $output->characters($mimetype);
+        $output->endElement($uriDevInf , 'CTType');
+        $output->startElement($uriDevInf , 'VerCT');
+        $output->characters($version);
+        $output->endElement($uriDevInf , 'VerCT');
+        $output->endElement($uriDevInf , 'Rx-Pref');
+
+        foreach ($additionaltypes as $ct => $ctver){
+            $output->startElement($uriDevInf , 'Rx');
+            $output->startElement($uriDevInf , 'CTType');
+            $output->characters($ct);
+            $output->endElement($uriDevInf , 'CTType');
+            $output->startElement($uriDevInf , 'VerCT');
+            $output->characters($ctver);
+            $output->endElement($uriDevInf , 'VerCT');
+            $output->endElement($uriDevInf , 'Rx');
+        }
+
+        $output->startElement($uriDevInf , 'Tx-Pref');
+        $output->startElement($uriDevInf , 'CTType');
+        $output->characters($mimetype);
+        $output->endElement($uriDevInf , 'CTType');
+        $output->startElement($uriDevInf , 'VerCT');
+        $output->characters($version);
+        $output->endElement($uriDevInf , 'VerCT');
+        $output->endElement($uriDevInf , 'Tx-Pref');
+
+        foreach ($additionaltypes as $ct => $ctver){
+            $output->startElement($uriDevInf , 'Tx');
+            $output->startElement($uriDevInf , 'CTType');
+            $output->characters($ct);
+            $output->endElement($uriDevInf , 'CTType');
+            $output->startElement($uriDevInf , 'VerCT');
+            $output->characters($ctver);
+            $output->endElement($uriDevInf , 'VerCT');
+            $output->endElement($uriDevInf , 'Tx');
+        }
+
+        $output->startElement($uriDevInf , 'SyncCap');
+        // We support all sync Types from 1-6: two way, slow, refresh|update
+        // from client|server
+        for ($i = 1; $i <= 6; ++$i) {
+            $output->startElement($uriDevInf , 'SyncType');
+            $output->characters($i);
+            $output->endElement($uriDevInf , 'SyncType');
+        }
+        $output->endElement($uriDevInf , 'SyncCap');
+        $output->endElement($uriDevInf , 'DataStore');
+    }
+
+    function outputAlert($alertCode, $clientDB = '', $serverDB = '', $lastAnchor = '', $nextAnchor = '')
+    {
+        $uriMeta = $_SESSION['SyncML.state']->uriMeta;
+
+        $this->_output->startElement($this->_uri, 'Alert');
+        $this->_outputCmdID();
+
+        $this->_output->startElement($this->_uri, 'Data');
+        $chars = $alertCode;
+        $this->_output->characters($chars);
+        $this->_output->endElement($this->_uri, 'Data');
+
+        $this->_output->startElement($this->_uri, 'Item');
+
+        if (!empty($clientDB)) {
+            $this->_output->startElement($this->_uri, 'Target');
+            $this->_output->startElement($this->_uri, 'LocURI');
+            $this->_output->characters($clientDB);
+            $this->_output->endElement($this->_uri, 'LocURI');
+            $this->_output->endElement($this->_uri, 'Target');
+        }
+
+        if (!empty($serverDB)) {
+            $this->_output->startElement($this->_uri, 'Source');
+            $this->_output->startElement($this->_uri, 'LocURI');
+            $this->_output->characters($serverDB);
+            $this->_output->endElement($this->_uri, 'LocURI');
+            $this->_output->endElement($this->_uri, 'Source');
+        }
+
+        $this->_output->startElement($this->_uri, 'Meta');
+
+        $this->_output->startElement($uriMeta, 'Anchor');
+
+        $this->_output->startElement($uriMeta, 'Last');
+        $this->_output->characters($lastAnchor);
+        $this->_output->endElement($uriMeta, 'Last');
+
+        $this->_output->startElement($uriMeta, 'Next');
+        $this->_output->characters($nextAnchor);
+        $this->_output->endElement($uriMeta, 'Next');
+
+        $this->_output->endElement($uriMeta, 'Anchor');
+
+
+        // MaxObjSize, required by protocol for SyncML1.1 and higher.
+        if ($_SESSION['SyncML.state']->version > 0) {
+            $this->_output->startElement($uriMeta, 'MaxObjSize');
+            $this->_output->characters(SERVER_MAXOBJSIZE);
+            $this->_output->endElement($uriMeta, 'MaxObjSize');
+        }
+        $this->_output->endElement($this->_uri, 'Meta');
+
+                $this->_output->endElement($this->_uri, 'Item');
+        $this->_output->endElement($this->_uri, 'Alert');
+
+    }
+
+
+    function outputGetDevInf()
+    {
+        $state = &$_SESSION['SyncML.state'];
+        $uriMeta = $state->uriMeta;
+
+        $this->_output->startElement($this->_uri, 'Get');
+        $this->_outputCmdID();
+
+        $this->_output->startElement($this->_uri, 'Meta');
+        $this->_output->startElement($uriMeta, 'Type');
+        if ($state->wbxml) {
+            $chars = MIME_SYNCML_DEVICE_INFO_WBXML;
+        } else {
+            $chars = MIME_SYNCML_DEVICE_INFO_XML;
+        }
+        $this->_output->characters($chars);
+        $this->_output->endElement($uriMeta, 'Type');
+        $this->_output->endElement($this->_uri, 'Meta');
+
+        $this->_output->startElement($this->_uri, 'Item');
+        $this->_output->startElement($this->_uri, 'Target');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        $this->_output->characters($state->getDevInfURI());
+        $this->_output->endElement($this->_uri, 'LocURI');
+        $this->_output->endElement($this->_uri, 'Target');
+        $this->_output->endElement($this->_uri, 'Item');
+
+        $this->_output->endElement($this->_uri, 'Get');
+    }
+
+    /**
+     * Creates a single Sync command
+     *
+     * @param string $command       The Sync command (Add, Delete, Replace).
+     * @param string $content       The actual object content.
+     * @param string $contentType   The content's MIME type.
+     * @param string $encodingType  The content encoding of the object.
+     * @param string $cuid          The client's object UID.
+     * @param string $suid          The server's object UID.
+     *
+     * @return integer  The CmdID used for this command.
+     */
+    function outputSyncCommand($command, $content = null, $contentType = null,
+                               $encodingType = null, $cuid = null, $suid = null)
+    {
+        $uriMeta = $_SESSION['SyncML.state']->uriMeta;
+
+        $this->_output->startElement($this->_uri, $command);
+        $this->_outputCmdID();
+
+        if (isset($contentType)) {
+            $this->_output->startElement($this->_uri, 'Meta');
+            $this->_output->startElement($uriMeta, 'Type');
+            $this->_output->characters($contentType);
+            $this->_output->endElement($uriMeta, 'Type');
+            $this->_output->endElement($this->_uri, 'Meta');
+        }
+
+        if (isset($content) || isset($cuid) || isset($suid)) {
+            $this->_output->startElement($this->_uri, 'Item');
+            if ($suid != null) {
+                $this->_output->startElement($this->_uri, 'Source');
+                $this->_output->startElement($this->_uri, 'LocURI');
+                $this->_output->characters($suid);
+                $this->_output->endElement($this->_uri, 'LocURI');
+                $this->_output->endElement($this->_uri, 'Source');
+            }
+
+            if ($cuid != null) {
+                $this->_output->startElement($this->_uri, 'Target');
+                $this->_output->startElement($this->_uri, 'LocURI');
+                $this->_output->characters($cuid);
+                $this->_output->endElement($this->_uri, 'LocURI');
+                $this->_output->endElement($this->_uri, 'Target');
+            }
+
+            if (!empty($encodingType)) {
+                $this->_output->startElement($this->_uri, 'Meta');
+                $this->_output->startElement($uriMeta, 'Format');
+                $this->_output->characters($encodingType);
+                $this->_output->endElement($uriMeta, 'Format');
+                $this->_output->endElement($this->_uri, 'Meta');
+            }
+            if (isset($content)) {
+                $this->_output->startElement($this->_uri, 'Data');
+                if($this->isWBXML()) {
+                    $this->_output->characters($content);
+                } else {
+                    $device = $_SESSION['SyncML.state']->getDevice();
+                    if ($device->useCdataTag()) {
+                        /* Enclose data in CDATA if possible to avoid */
+                        /* problems with &,< and >. */
+                        $this->_output->characters('<![CDATA[' . $content . ']]>');
+                    } else {
+                        $this->_output->characters($content);
+                    }
+                }
+                $this->_output->endElement($this->_uri, 'Data');
+            }
+            $this->_output->endElement($this->_uri, 'Item');
+        }
+
+        $this->_output->endElement($this->_uri, $command);
+
+        return $this->_msg_CmdID - 1;
+    }
+
+    function outputSyncStart($clientLocURI, $serverLocURI, $numberOfChanges = null)
+    {
+        $this->_output->startElement($this->_uri, 'Sync');
+        $this->_outputCmdID();
+
+        $this->_output->startElement($this->_uri, 'Target');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        $this->_output->characters($clientLocURI);
+        $this->_output->endElement($this->_uri, 'LocURI');
+        $this->_output->endElement($this->_uri, 'Target');
+
+        $this->_output->startElement($this->_uri, 'Source');
+        $this->_output->startElement($this->_uri, 'LocURI');
+        $this->_output->characters($serverLocURI);
+        $this->_output->endElement($this->_uri, 'LocURI');
+        $this->_output->endElement($this->_uri, 'Source');
+
+        if (is_int($numberOfChanges)) {
+            $this->_output->startElement($this->_uri, 'NumberOfChanges');
+            $this->_output->characters($numberOfChanges);
+            $this->_output->endElement($this->_uri, 'NumberOfChanges');
+        }
+
+    }
+
+    function outputSyncEnd()
+    {
+        $this->_output->endElement($this->_uri, 'Sync');
+    }
+
+
+    //  internal helper functions:
+
+    function _outputCmdID()
+    {
+        $this->_output->startElement($this->_uri, 'CmdID');
+        $this->_output->characters($this->_msg_CmdID);
+        $this->_msg_CmdID++;
+        $this->_output->endElement($this->_uri, 'CmdID');
+    }
+
+    /**
+     * Output a single <ele>$str</ele> element.
+     */
+    function _singleEle($tag, $str, $uri = null)
+    {
+        if (empty($uri)) {
+            $uri = $this->_uri;
+        }
+        $this->_output->startElement($uri, $tag);
+        $this->_output->characters($str);
+        $this->_output->endElement($uri, $tag);
+    }
+
+}
diff --git a/framework/SyncML/docs/INSTALL.p900.txt b/framework/SyncML/docs/INSTALL.p900.txt
new file mode 100644 (file)
index 0000000..7f16140
--- /dev/null
@@ -0,0 +1,54 @@
+
+
+How to get SyncML working with your P800/P900:
+[[somebody with access to other phones might add instructions
+for these as well. Thanks]]
+
+Start "Remote Synchronisation" from you phones app launcher:
+
+
+1)Select edit/settings and put in the following:
+
+"Server" tab:
+server address: http://yourserver.com/horde/rpc.php
+user name: your horde user name
+password: your horde user password
+
+"Protocol" tab:
+Needs not to be filled in. Unless you have your web-server
+configured to require HTTP Authentification.
+
+
+2) Configure sync types:
+
+Start with Calendar/Tasks:
+
+       check "activate"
+       name can be anything
+       server database: must be "calendar"
+
+For Jotter the server database name is "notes". 
+
+However you should start with activating one type only.
+
+3) Hit Sync, wait and hope.
+
+Now Synchronoisation should take place. During the first sync
+a set union of the client's and server's data sets are created.
+Further Sync runs should only transfer the changes since last
+time.
+
+
+If it's not working:
+
+make sure the /tmp/sync directory exists and is writeable by your
+webserver (user might be wwwrun). Try to Sync once more and check
+the input/output in this directory. 
+Maybe change your php.ini settings so php logs ERROR messages and
+NOTICEs in a file as well. Check this file. Your phone gets 
+confused when the XML is cluttered with stuff like
+
+"PHP Notice:  Undefined index:  body in /blah/blub.php on line 256"
+
+Consult sync@lists.horde.org and the syncml.org protocol specification.
+
diff --git a/framework/SyncML/docs/README.program_flow.txt b/framework/SyncML/docs/README.program_flow.txt
new file mode 100644 (file)
index 0000000..0c2d115
--- /dev/null
@@ -0,0 +1,121 @@
+##
+## $Horde: framework/SyncML/docs/README.program_flow.txt,v 1.4 2006/09/10 04:55:06 chuck Exp $
+##
+
+rpc.php in horde's main directory is the starting point for our (and any)
+RPC call.
+
+It determines the $serverType ("syncml" for us) and then does something
+like this:
+
+$server = Horde_RPC::factory($serverType); // [will include RPC/syncml.php and create the class therein]
+$server->authorize();
+$input = $server->getInput(); // [basically the HTTP POST data]
+$out = $server->getResponse($input, $params);
+echo $out
+
+So the main part takes place in getResponse of
+framework/RPC/RPC/syncml.php's Horde_RPC_syncml class and there in
+getResponse:
+
+
+First, XML_WBXML_ContentHandler is installed as an output content handler:
+
+$this->_output = new XML_WBXML_ContentHandler();
+
+Despite the name, this class has (almost) nothing to do with WBXML.
+It's a helper to produce xml. To do this, it has 4 main methods:
+
+1) startElement($uri, $element, $attrs) produces an <$element xlmns=$uri
+   attr1=v1 ...> opening tag
+2) characters($str) addes $str to the content
+3) endElement($uri, $element) produces a closing tag </$element>
+4) getOutput() returns the output produced so far
+
+All subsequent code produces output by calling functions 1)-3)
+
+After installing the output content handler, Horde_RPC_syncml::getResponse
+continues with
+
+$this->_parse($request);
+
+do do the actual parsing and output creation and then finally
+
+$xmlinput = $this->_output->getOutput();
+
+to retrieve the created output from the content handler.
+The name $xmlinput is misleading, it should be called xmloutput instead.
+
+So our quest for the code continues withing the Horde_RPC_syncml's _parse
+function:
+
+It creates an XML Parser and registers the class (well, the object) itself
+as element handlers:
+_startElement,_endElement, and _characters, which only format the data a
+bit and call startElement,endElement, and characters respectively.
+
+Please note, that start/endElment sounding functions are used for processing
+the input as well as for creation of the output.
+This can be somewhat confusing. As a rule of thumb, code that produces xml
+output contains reference to an output var and looks like this:
+
+$this->_output->startElement(...);
+
+After the XML parser is istalled, it is fired and the execution takes place
+in the element handler functions.
+
+A syncml message (input as well as output) has this structure:
+<SyncML>
+  <SyncHdr>
+    ...stuff...
+  </SyncHdr>
+  <SyncBody>
+    ...stuff...
+  </SyncBody>
+<SyncML>
+
+the content handler in Horde_RPC_syncml delegate the work for header and
+body to the two sub-content handlers SyncML_SyncMLHdr and
+SyncML_SyncMLBody which reside in framework/SyncML/SyncML.php.
+So at least we made it to the to the SyncML package by now...
+
+The job of SyncML_SyncMLHdr is to read all the values in the header
+and store them in a php session (custom session, not normal horde session
+system) of type SyncML_State. After all header data is collected,
+outputSyncHdr write a SyncHdr as output.
+
+SyncML_SyncMLBody is another delegator. First it creates a
+SyncML_Command_Status to output the status-code of the session
+(authorized or not).
+The content of the <syncBody> element are command(-tags): for each element
+in there, an appropriate handler is created with
+SyncML_Command::factory($element);
+and assigned the tasks of handling this command.
+So execution continues with classes in SyncML/Command/ which are
+all children of SyncML_Command.
+
+>From here, you're on your own. Just two more facts:
+
+1)
+processing of changes received from the client are handled in
+SyncML/Sync.php (not to be confused with SyncML/Command/Sync.php) and
+there in runSyncCommand($command) command is one of
+SyncML_Command_Sync_(Add|Delete|Replace)
+
+2)
+The other way around:
+creating changes on the server for the client is done after the changes
+from the client have been processed. This is done in TwoWaySync.php.
+Some care has to be taken to avoid that the changes that are received
+from the client are considered "new changes" and echoed back to the
+client. That would result in severe data duplication meltdown.
+
+
+Files in SymcML:
+
+./SyncML.php:
+       definition of SyncML_ContentHandler, parent for Header- and Body-Handler
+       SyncML_SyncMLHdr
+               getStateFromSession() initialize session
+               outputSyncHdr() write the Sync Header
+       SyncML_SyncMLBody
diff --git a/framework/SyncML/docs/README.syncml_primer.txt b/framework/SyncML/docs/README.syncml_primer.txt
new file mode 100644 (file)
index 0000000..fcd2ebe
--- /dev/null
@@ -0,0 +1,142 @@
+
+
+SyncML Primer:
+--------------
+
+A SyncML Protocol Primer
+
+The specification can be downloaded from www.syncml.org.
+This Primer deals with SyncML 1.0.1 only.
+
+Basically a SynML Synchronisations consists of 6 steps: Three packages sent 
+from the Client to the Server and three packages the other way round.
+
+Here's a brief description of these 2*3 steps. The Chapter references refer 
+to XML examples for these steps in SyncML Sync Protocol, version 1.0.1(pdf) 
+from syncml.org.  I found these examples most helpful.
+
+Here we go:
+
+1a) Sync Initialization Package from Client (Chapter 4.1.1)
+
+Client starts communication, sends authentification and device info and maybe 
+info about previous sync if any.
+
+1b) Sync Initialization Package from Server (chapter 4.2.1)
+
+Server responds with session info if authorisation was successfull, provides 
+device info if requested and the synchronisation type (like TwoWaySync or 
+SlowSync) that is suitable for this run. Basically, if both sides "remember" 
+the same timestamp for the previous sync run, a TwoWaySync can be used to 
+transfer only the differences since then. Otherwise or for initial sync, 
+a SlowSync is used. In that the client sends all its data to the server 
+which then handles them.
+
+2a) Client Sending Modifications to Server (Chapter 5.1.1)
+
+The client sends all its modifications since the last sync run 
+(or all data for SlowSync) to the server
+
+2b) Server Sending Modifications to Client
+
+The server incoporates the changes from the client and now sends its 
+modifications to the client.
+
+3a) Data Update Status to Server (Chapter 5.3.1)
+
+A key concept of SyncML is that client and server have their own internal 
+representation of the data and use different primary keys. To identify 
+items there has to be a mapping between the client's keys and the server's 
+keys. (primary keys are relative URIs in SyncML language).  This map is
+maintained by the server. After the client has incoporated the servers data
+in its own database it sends its new primary keys (<source><LocURI>) for the
+changed data back to the server.  The server can then update its map.
+
+3b) Map Acknowledgement from Server (Chapter 5.4.1)
+
+Basically says: "Whoopie, we're through. See you next time".
+
+
+XML Specification:
+
+
+Each SyncML Packet consists of one SyncMLHdr Element an one SyncMLBody Element.
+
+The header contains authorisation and session information.
+A typical header sent from the server might look like this:
+
+<SyncHdr>
+  <VerDTD>1.0</VerDTD>
+  <VerProto>SyncML/1.0</VerProto>
+  <SessionID>424242424242</SessionID>
+  <MsgID>2</MsgID>
+  <Target>
+   <LocURI>111111-00-222222-4</LocURI>
+  </Target>
+  <Source>
+    <LocURI>http://mysyncmlserver.com/horde/rpc.php</LocURI>
+  </Source>
+  <RespURI>http://mysyncmlserver.com/horde/rpc.php</RespURI>
+</SyncHdr>
+
+The SyncBody contains the following elements (called "commands") as specified
+in the DTD:
+
+(Alert | Atomic | Copy | Exec | Get | Map | Put | Results | Search | Sequence
+| Status | Sync)+, Final?
+
+CmdID: each command in a packet has a unique command id like <CmdID>1</CmdId>
+
+We discuss only Alert,Get,Put,Results,Map,Status Sync and Final here.
+
+
+Get
+
+The Get request command works similar to HTTP GET: it is intended to request
+data from the communication partner.  Currently it's only use is to retrieve
+"./devinf10" (or 11 for syncml 1.1) which contains information about the sync
+capabilitys of the partner.
+
+Put
+
+Put is similar to HTTP POST: it's designed to transfer data to the
+communication partner. As with get, the only use at the moment is to publish
+the "./devinf10" device information to the communication partner. A typcial 
+first packet from the client would include a GET for the servers devinf and a 
+put with the client's own devinf data.
+
+Result
+
+The Result Element is used to respond to a GET Command and contains the 
+requested data, i.e. the devinf data.
+
+Status
+
+In General, for each command there must be a status response from the other 
+side. (For exception see the spec.)
+
+The Status includes a CmdID (like any command).  It has a MsgRef and CmdRef to
+identify the command it responds to: MsgRef identifies the packet (given in
+the Header) and CmdRef the CmdId of the original command. There's also a <cmd>
+Element to specify the type
+
+<Status>
+  <CmdID>3</CmdID>
+  <MsgRef>1</MsgRef>
+  <CmdRef>2</CmdRef>
+  <Cmd>Put</Cmd>
+  <SourceRef>./devinf10</SourceRef>
+  <Data>200</Data> <!--Statuscode for OK-->
+</Status>
+
+Sync
+
+Alert
+
+Sync and Alert is where the action takes place. Unfortunately the primer is 
+not yet finished.
+Stay tuned or check the Spec yourself... 
+
+
+
+
diff --git a/framework/SyncML/docs/TODO b/framework/SyncML/docs/TODO
new file mode 100644 (file)
index 0000000..3d48d9b
--- /dev/null
@@ -0,0 +1,34 @@
+TODOs
+-----
+
+- handle recurring events
+- handle alarms in sync4j
+
+- review session handling. 
+   - Deal with "unfinished" sessions after incomplete sync.
+   - use cookies where possible. Otherwise concurrent sessions with
+     device ID "sc-pim-outlook" will cause trouble big time.
+
+- implement BUSY feature
+
+- try to speedup data retrival inside horde
+
+- create configuration in horde preferences: allow fancy stuff like
+  "delete calendar entries from clients for events n days in the past"
+- create page to view summary of previous sync and ultimately handle
+  collisions as well...
+
+- get vcard/icalendar handling right: support ical 1.0/2.0 and vcard
+  2.1/3.0 creation.
+  
+Redesign / Refactoring
+----------------------
+
+
+-concentrate business logic where possible (controller class),
+ seperate xml parsing/creation from business logic
+
+$Horde: framework/SyncML/docs/TODO,v 1.28 2005/05/05 06:29:31 karsten Exp $
+       
\ No newline at end of file
diff --git a/framework/SyncML/package.xml b/framework/SyncML/package.xml
new file mode 100644 (file)
index 0000000..b76c516
--- /dev/null
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>SyncML</name>
+ <channel>pear.horde.org</channel>
+ <summary>SyncML provides an API for processing SyncML requests.</summary>
+ <description>This package provides classes for implementing a SyncML server.
+ </description>
+ <lead>
+  <name>Karsten Fourmont</name>
+  <user>karsten</user>
+  <email>karsten@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2007-01-11</date>
+ <time>13:58:57</time>
+ <version>
+  <release>0.7.0</release>
+  <api>0.7.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Make the SyncML package independant from Horde, provide a new Sql backend as a reference implementation for a backend.</notes>
+ <contents>
+  <dir name="/">
+   <dir name="docs">
+    <file name="INSTALL.p900.txt" role="doc" />
+    <file name="README.program_flow.txt" role="doc" />
+    <file name="README.syncml_primer.txt" role="doc" />
+    <file name="TODO" role="doc" />
+   </dir> <!-- /docs -->
+   <dir name="SyncML">
+    <dir name="Command">
+     <file name="Alert.php" role="php" />
+     <file name="Final.php" role="php" />
+     <file name="Get.php" role="php" />
+     <file name="Map.php" role="php" />
+     <file name="Put.php" role="php" />
+     <file name="Results.php" role="php" />
+     <file name="Status.php" role="php" />
+     <file name="Sync.php" role="php" />
+     <file name="SyncHdr.php" role="php" />
+     <file name="SyncElement.php" role="php" />
+    </dir> <!-- /SyncML/Command -->
+    <dir name="Device">
+     <file name="Nokia.php" role="php" />
+     <file name="P800.php" role="php" />
+     <file name="Sync4j.php" role="php" />
+     <file name="Synthesis.php" role="php" />
+     <file name="Sync4JMozilla.php" role="php" />
+    </dir> <!-- /SyncML/Device -->
+    <dir name="Backend">
+     <file name="Horde.php" role="php" />
+     <file name="Sql.php" role="php" />
+    </dir> <!-- /SyncML/Backend -->
+    <file name="Backend.php" role="php" />
+    <file name="Command.php" role="php" />
+    <file name="Constants.php" role="php" />
+    <file name="Device.php" role="php" />
+    <file name="DeviceInfo.php" role="php" />
+    <file name="State.php" role="php" />
+    <file name="Sync.php" role="php" />
+    <file name="XMLOutput.php" role="php" />
+   </dir> <!-- /SyncML -->
+   <file name="SyncML.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>XML_WBXML</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+   <release>
+   <version>
+    <release>0.6.0</release>
+    <api>0.6.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-01-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Passes all tests of SyncML conformance suite now.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2005-04-23</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Major update
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-19</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial implementation
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/SyncML/tests/sif.phpt b/framework/SyncML/tests/sif.phpt
new file mode 100644 (file)
index 0000000..8fe21d9
--- /dev/null
@@ -0,0 +1,161 @@
+--TEST--
+SIF tests.
+--FILE--
+<?php
+
+// Setup stubs.
+class BackendStub {
+    function logMessage() {}
+}
+$backend = new BackendStub();
+define('PEAR_LOG_DEBUG', null);
+
+// Load device handler.
+require_once dirname(__FILE__) . '/../SyncML/Device.php';
+$device = SyncML_Device::factory('Sync4j');
+
+$data = <<<EVENT
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:cdillon's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20080630T110000Z
+DTEND:20080630T120000Z
+DTSTAMP:20080630T201939Z
+UID:20080630151854.190949aaovgixvhq@www.wolves.k12.mo.us
+CREATED:20080630T201854Z
+LAST-MODIFIED:20080630T201854Z
+SUMMARY:Server02
+ORGANIZER;CN=Chris Dillon:mailto:cdillon@wolves.k12.mo.us
+CLASS:PUBLIC
+STATUS:CONFIRMED
+TRANSP:OPAQUE
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="Dillon,
+  Chris":mailto:cdillon@wolves.k12.mo.us
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DURATION:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EVENT;
+
+echo $device->vevent2sif($data);
+echo "\n\n";
+
+$data = <<<CONTACT
+<?xml version="1.0" encoding="UTF-8"?>
+<contact>
+<Anniversary/>
+<AssistantName/>
+<AssistantTelephoneNumber/>
+<BillingInformation/>
+<Birthday>2008-10-18</Birthday>
+<Body>Comments
+More comments
+And just a couple more</Body>
+<Business2TelephoneNumber/>
+<BusinessAddressCity>Golden Hills</BusinessAddressCity>
+<BusinessAddressCountry>Australia</BusinessAddressCountry>
+<BusinessAddressPostOfficeBox/>
+<BusinessAddressPostalCode>4009</BusinessAddressPostalCode>
+<BusinessAddressState>Qld</BusinessAddressState>
+<BusinessAddressStreet>Company
+Unit 2, 123 St Freds Tce</BusinessAddressStreet>
+<BusinessFaxNumber/>
+<BusinessTelephoneNumber>+61 712341234</BusinessTelephoneNumber>
+<CallbackTelephoneNumber/>
+<CarTelephoneNumber/>
+<Categories/>
+<Children/>
+<Companies/>
+<CompanyMainTelephoneNumber/>
+<CompanyName>Company</CompanyName>
+<ComputerNetworkName/>
+<Department/>
+<Email1Address>test@domain.com</Email1Address>
+<Email1AddressType>SMTP</Email1AddressType>
+<Email2Address>user@seconddomain.com</Email2Address>
+<Email2AddressType>SMTP</Email2AddressType>
+<Email3Address/>
+<Email3AddressType/>
+<FileAs>Lastname, Firstname</FileAs>
+<FirstName>Firstname</FirstName>
+<Folder>DEFAULT_FOLDER</Folder>
+<Gender>0</Gender>
+<Hobby/>
+<Home2TelephoneNumber/>
+<HomeAddressCity/>
+<HomeAddressCountry/>
+<HomeAddressPostOfficeBox/>
+<HomeAddressPostalCode/>
+<HomeAddressState/>
+<HomeAddressStreet/>
+<HomeFaxNumber/>
+<HomeTelephoneNumber/>
+<HomeWebPage/>
+<IMAddress/>
+<Importance>1</Importance>
+<Initials>F.L.</Initials>
+<JobTitle/>
+<Language/>
+<LastName>Lastname</LastName>
+<MailingAddress>Company
+Unit 2, 123 St Freds Tce
+Golden Hills  Qld  4009
+Australia</MailingAddress>
+<ManagerName/>
+<MiddleName/>
+<Mileage/>
+<MobileTelephoneNumber>+61 123123123</MobileTelephoneNumber>
+<NickName/>
+<OfficeLocation/>
+<OrganizationalIDNumber/>
+<OtherAddressCity/>
+<OtherAddressCountry/>
+<OtherAddressPostOfficeBox/>
+<OtherAddressPostalCode/>
+<OtherAddressState/>
+<OtherAddressStreet/>
+<OtherFaxNumber/>
+<OtherTelephoneNumber/>
+<PagerNumber/>
+<Photo/>
+<PrimaryTelephoneNumber/>
+<Profession/>
+<RadioTelephoneNumber/>
+<Sensitivity>0</Sensitivity>
+<Spouse/>
+<Subject>Firstname Lastname</Subject>
+<Suffix/>
+<TelexNumber/>
+<Title/>
+<WebPage/>
+<YomiCompanyName/>
+<YomiFirstName/>
+<YomiLastName/>
+</contact>
+CONTACT;
+
+echo $device->sif2vcard($data);
+
+?>
+--EXPECT--
+<?xml version="1.0"?><appointment><ReminderSet>1</ReminderSet><IsRecurring>0</IsRecurring><BusyStatus>2</BusyStatus><AllDayEvent>0</AllDayEvent><Start>20080630T110000Z</Start><End>20080630T120000Z</End><Subject>Server02</Subject><Sensitivity>0</Sensitivity><ReminderMinutesBeforeStart>15</ReminderMinutesBeforeStart><Duration>60</Duration></appointment>
+
+BEGIN:VCARD
+VERSION:3.0
+FN:Lastname\, Firstname
+TEL;TYPE=WORK:+61 712341234
+TEL;TYPE=CELL:+61 123123123
+EMAIL:test@domain.com
+EMAIL:user@seconddomain.com
+NOTE:Comments\nMore comments\nAnd just a couple more
+BDAY:2008-10-18
+N:Lastname;Firstname;;;
+ADR;TYPE=WORK:;;Company\nUnit 2\, 123 St Freds Tce;Golden
+  Hills;Qld;4009;Australia
+ORG:Company
+END:VCARD
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/data.txt b/framework/SyncML/tests/testcase_P900_tasksandcalendar/data.txt
new file mode 100644 (file)
index 0000000..9393d4f
--- /dev/null
@@ -0,0 +1,565 @@
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:667
+SUMMARY:client1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205000Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:client1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205000Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:678
+SUMMARY:cIient1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART:20061225T000000Z
+DTEND:20061225T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:cIient1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART;VALUE=DATE:20061225
+DTEND;VALUE=DATE:20061225
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:679
+SUMMARY:clienttask1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+CATEGORIES:X-65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:clienttask1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+CATEGORIES:65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070101T000000
+DTEND:20070101T235959
+DTSTAMP:20070102T180655Z
+UID:20070102190218.5fbc16hzzroc@voltaire.local
+SUMMARY:server1allday new year
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+LOCATION:everywhere
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+X-EPOCAGENDAENTRYTYPE:EVENT
+DTSTART:20070101T000000
+DTEND:20070102T000000
+DTSTAMP:20070102T180655Z
+SUMMARY:server1allday new year
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+LOCATION:everywhere
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+DTSTAMP:20070102T180655Z
+UID:20070102190234.3wz9alsndh0k@voltaire.local
+SUMMARY:server2 6-7
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+DTSTAMP:20070102T180655Z
+SUMMARY:server2 6-7
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+UID:20070102183704.6l75i6x9a2gw@voltaire.local
+ORGANIZER:syncmltest
+SUMMARY:servertask1
+DESCRIPTION:
+PRIORITY:3
+STATUS:NEEDS ACTION
+CATEGORIES:65536
+LAST-MODIFIED:20070102T180247Z
+END:VTODO
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:servertask1
+DESCRIPTION:
+PRIORITY:3
+STATUS:NEEDS ACTION
+CATEGORIES:65536
+LAST-MODIFIED:20070102T180247Z
+END:VTODO
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:680
+SUMMARY:server1alldayc1 new year
+DTSTART:20070101T000000Z
+DTEND:20070101T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+LOCATION:everywhere
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205600Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:server1alldayc1 new year
+DTSTART;VALUE=DATE:20070101
+DTEND;VALUE=DATE:20070101
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+LOCATION:everywhere
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205600Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:681
+SUMMARY:server2c1 6-7
+DESCRIPTION:alarm half hour before.
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20070102T043000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:server2c1 6-7
+DESCRIPTION:alarm half hour before.
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20070102T043000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:682
+SUMMARY:servertask1c1
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205500Z
+CATEGORIES:X-65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:servertask1c1
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205500Z
+CATEGORIES:65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+DTSTAMP:20070102T181054Z
+UID:20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+SUMMARY:client1s1 12-1
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+AALARM:20061222T105000Z
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+DTSTAMP:20070102T181054Z
+SUMMARY:client1s1 12-1
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+AALARM:20061222T105000Z
+END:VEVENT
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20061225T000000
+DTEND:20061225T235959
+DTSTAMP:20070102T181054Z
+UID:20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org
+SUMMARY:cIient1s1 christmas all day
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+uumlaut: =C3=BC
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+X-EPOCAGENDAENTRYTYPE:EVENT
+DTSTART:20061225T000000
+DTEND:20061226T000000
+DTSTAMP:20070102T181054Z
+SUMMARY:cIient1s1 christmas all day
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+uumlaut: =C3=BC
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output received from horde backend (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+UID:20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+ORGANIZER:syncmltest
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+Aumlaut: =C3=84
+PRIORITY:5
+STATUS:NEEDS ACTION
+CATEGORIES:65536
+DCREATED:20070102T180651Z
+LAST-MODIFIED:20070102T180740Z
+END:VTODO
+END:VCALENDAR
+
+
+output converted for client (text/x-vcalendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+Aumlaut: =C3=84
+PRIORITY:5
+STATUS:NEEDS ACTION
+CATEGORIES:65536
+DCREATED:20070102T180651Z
+LAST-MODIFIED:20070102T180740Z
+END:VTODO
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:667
+SUMMARY:client1s1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\DefaultAlarm\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:client1s1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\DefaultAlarm\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:678
+SUMMARY:cIient1s1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART:20061225T000000Z
+DTEND:20061225T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+SUMMARY:cIient1s1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART;VALUE=DATE:20061225
+DTEND;VALUE=DATE:20061225
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+
+
+input received from client (text/calendar)
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:679
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20070101T230000Z
+LAST-MODIFIED:20061222T205700Z
+CATEGORIES:X-65536
+PRIORITY:5
+END:VTODO
+END:VCALENDAR
+
+
+input converted for server (text/calendar):
+BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20070101T230000Z
+LAST-MODIFIED:20061222T205700Z
+CATEGORIES:65536
+PRIORITY:5
+END:VTODO
+END:VCALENDAR
+
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/devinf.txt b/framework/SyncML/tests/testcase_P900_tasksandcalendar/devinf.txt
new file mode 100644 (file)
index 0000000..c37c317
--- /dev/null
@@ -0,0 +1,278 @@
+SyncML_DeviceInfo::__set_state(array(
+   '_VerDTD' => '1.0',
+   '_Man' => 'Sony Ericsson',
+   '_Mod' => 'P900',
+   '_OEM' => 'Symbian',
+   '_FwV' => 'R5B02',
+   '_SwV' => '1.0',
+   '_HwV' => 'R1A',
+   '_DevID' => '351965-00-340413-3',
+   '_DevTyp' => 'smartphone',
+   '_DataStore' => 
+  array (
+    0 => 
+    SyncML_DataStore::__set_state(array(
+       '_SourceRef' => 'c:\\Documents\\agenda\\agenda',
+       '_DisplayName' => NULL,
+       '_MaxGUIDSize' => '4',
+       '_Rx_Pref' => 
+      array (
+        'text/x-vcalendar' => '1.0',
+      ),
+       '_Rx' => NULL,
+       '_Tx_Pref' => 
+      array (
+        'text/x-vcalendar' => '1.0',
+      ),
+       '_Tx' => NULL,
+       '_DSMem' => NULL,
+       '_SyncCap' => 
+      array (
+        1 => true,
+        2 => true,
+      ),
+    )),
+  ),
+   '_CTCap' => 
+  array (
+    'text/x-vcalendar' => 
+    array (
+      'AALARM' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'ATTACH' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'ATTENDEE' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => 
+        array (
+          'EXPECT' => 
+          SyncML_PropertyParameter::__set_state(array(
+             '_ValEnum' => NULL,
+             '_DataType' => NULL,
+             '_Size' => NULL,
+             '_DisplayName' => NULL,
+          )),
+          'ROLE' => 
+          SyncML_PropertyParameter::__set_state(array(
+             '_ValEnum' => NULL,
+             '_DataType' => NULL,
+             '_Size' => NULL,
+             '_DisplayName' => NULL,
+          )),
+          'RSVP' => 
+          SyncML_PropertyParameter::__set_state(array(
+             '_ValEnum' => NULL,
+             '_DataType' => NULL,
+             '_Size' => NULL,
+             '_DisplayName' => NULL,
+          )),
+          'STATUS' => 
+          SyncML_PropertyParameter::__set_state(array(
+             '_ValEnum' => NULL,
+             '_DataType' => NULL,
+             '_Size' => NULL,
+             '_DisplayName' => NULL,
+          )),
+        ),
+      )),
+      'BEGIN' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => 
+        array (
+          'VCALENDAR' => true,
+          'VEVENT' => true,
+          'VTODO' => true,
+        ),
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'CATEGORIES' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'COMPLETED' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'CLASS' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => 
+        array (
+          'PUBLIC' => true,
+          'PRIVATE' => true,
+          'CONFIDENTIAL' => true,
+        ),
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DAYLIGHT' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DCREATED' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DESCRIPTION' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DTSTART' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DTEND' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'DUE' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'END' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => 
+        array (
+          'VEVENT' => true,
+          'VCALENDAR' => true,
+          'VTODO' => true,
+        ),
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'EXDATE' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'LAST-MODIFIED' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'LOCATION' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'PRIORITY' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'RRULE' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'STATUS' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'SUMMARY' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'UID' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => NULL,
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+      'VERSION' => 
+      SyncML_Property::__set_state(array(
+         '_ValEnum' => 
+        array (
+          '1.0' => true,
+        ),
+         '_DataType' => NULL,
+         '_Size' => NULL,
+         '_DisplayName' => NULL,
+         '_params' => NULL,
+      )),
+    ),
+  ),
+   '_Ext' => NULL,
+   '_UTC' => NULL,
+   '_supportLargeObjs' => NULL,
+   '_supportNumberOfChanges' => NULL,
+))
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_10.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..b7bfb3e
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T204212Z</Last><Next>20061222T205323Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_11.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..b028ab8
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761196</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Replace><CmdID>5</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>667</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:667
+SUMMARY:client1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205000Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>6</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>678</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:678
+SUMMARY:cIient1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART:20061225T000000Z
+DTEND:20061225T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>7</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>679</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:679
+SUMMARY:clienttask1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205200Z
+CATEGORIES:X-65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_12.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..8e34794
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>20070102190218.5fbc16hzzroc@voltaire.local</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Add</Cmd><SourceRef>20070102190234.3wz9alsndh0k@voltaire.local</SourceRef><Data>201</Data></Status><Status><CmdID>5</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Add</Cmd><SourceRef>20070102183704.6l75i6x9a2gw@voltaire.local</SourceRef><Data>201</Data></Status><Map><CmdID>6</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><MapItem><Target><LocURI>20070102190218.5fbc16hzzroc@voltaire.local</LocURI></Target><Source><LocURI>680</LocURI></Source></MapItem></Map><Map><CmdID>7</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><MapItem><Target><LocURI>20070102190234.3wz9alsndh0k@voltaire.local</LocURI></Target><Source><LocURI>681</LocURI></Source></MapItem></Map><Map><CmdID>8</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><MapItem><Target><LocURI>20070102183704.6l75i6x9a2gw@voltaire.local</LocURI></Target><Source><LocURI>682</LocURI></Source></MapItem></Map><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_20.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..f9fa7a6
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T205323Z</Last><Next>20061222T205735Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_21.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..b51a91b
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761447</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Replace><CmdID>5</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>680</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:680
+SUMMARY:server1alldayc1 new year
+DTSTART:20070101T000000Z
+DTEND:20070101T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+LOCATION:everywhere
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205600Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>6</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>681</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:681
+SUMMARY:server2c1 6-7
+DESCRIPTION:alarm half hour before.
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20070102T043000Z;;0;c:\system\data\themes\sound\Reminder\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>7</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>682</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:682
+SUMMARY:servertask1c1
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205500Z
+CATEGORIES:X-65536
+PRIORITY:1
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_22.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..860e178
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><TargetRef>667</TargetRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><TargetRef>678</TargetRef><Data>200</Data></Status><Status><CmdID>5</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Replace</Cmd><TargetRef>679</TargetRef><Data>200</Data></Status><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_30.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..2a1bf68
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T205735Z</Last><Next>20061222T205851Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_31.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..c919d63
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761524</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Replace><CmdID>5</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>667</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:667
+SUMMARY:client1s1 12-1
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+X-EPOCAGENDAENTRYTYPE:APPOINTMENT
+AALARM;TYPE=X-EPOCSOUND:20061222T105000Z;;0;c:\system\data\themes\sound\DefaultAlarm\Classical.mp3
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>6</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>678</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VEVENT
+UID:678
+SUMMARY:cIient1s1 christmas all day
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:uumlaut:=20=C3=BC
+DTSTART:20061225T000000Z
+DTEND:20061225T240000Z
+X-EPOCAGENDAENTRYTYPE:EVENT
+CLASS:PUBLIC
+DCREATED:20061221T230000Z
+LAST-MODIFIED:20061222T205700Z
+PRIORITY:0
+STATUS:NEEDS ACTION
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>7</CmdID><Meta><Type xmlns='syncml:metinf'>text/calendar</Type></Meta><Item><Source><LocURI>679</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+UID:679
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Aumlaut:=20=C3=84
+X-EPOCTODOLIST:To-do list
+STATUS:NEEDS ACTION
+X-EPOCAGENDAENTRYTYPE:TODO
+CLASS:PUBLIC
+DCREATED:20070101T230000Z
+LAST-MODIFIED:20061222T205700Z
+CATEGORIES:X-65536
+PRIORITY:5
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_32.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..7ef7166
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_40.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..4fad91c
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T205851Z</Last><Next>20061222T210039Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_41.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..1393a6b
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761653</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_42.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..f26235d
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_50.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_50.xml
new file mode 100644 (file)
index 0000000..0f53560
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T210039Z</Last><Next>20061222T210325Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_51.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_51.xml
new file mode 100644 (file)
index 0000000..2325736
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761797</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Delete><CmdID>5</CmdID><Item><Source><LocURI>681</LocURI></Source></Item></Delete><Delete><CmdID>6</CmdID><Item><Source><LocURI>682</LocURI></Source></Item></Delete></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_52.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_52.xml
new file mode 100644 (file)
index 0000000..74924e8
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><TargetRef>667</TargetRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Delete</Cmd><TargetRef>679</TargetRef><Data>200</Data></Status><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_60.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_60.xml
new file mode 100644 (file)
index 0000000..98009bf
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>1</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Cred><Meta><Format xmlns='syncml:metinf'>b64</Format><Type xmlns='syncml:metinf'>syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns='syncml:devinf'><VerDTD>1.0</VerDTD>
+<Man>Sony Ericsson</Man>
+<Mod>P900</Mod>
+<OEM>Symbian</OEM>
+<FwV>R5B02</FwV>
+<SwV>1.0</SwV>
+<HwV>R1A</HwV>
+<DevTyp>smartphone</DevTyp>
+<DevID>351965-00-340413-3</DevID><DataStore><SourceRef>c:\Documents\agenda\agenda</SourceRef>
+
+       <MaxGUIDSize>4</MaxGUIDSize>
+       <Rx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+               <CTType>text/x-vcalendar</CTType>
+               <VerCT>1.0</VerCT>
+       </Tx-Pref>
+       <DSMem>
+       </DSMem>
+       <SyncCap>
+               <SyncType>1</SyncType>
+               <SyncType>2</SyncType>
+       </SyncCap>
+</DataStore>
+<CTCap>
+       <CTType>text/x-vcalendar</CTType>
+       <PropName>AALARM</PropName>
+       <PropName>ATTACH</PropName>
+       <PropName>ATTENDEE</PropName>
+               <ParamName>EXPECT</ParamName>
+               <ParamName>ROLE</ParamName>
+               <ParamName>RSVP</ParamName>
+               <ParamName>STATUS</ParamName>
+       <PropName>BEGIN</PropName>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>CATEGORIES</PropName>
+       <PropName>COMPLETED</PropName>
+       <PropName>CLASS</PropName>
+               <ValEnum>PUBLIC</ValEnum>
+               <ValEnum>PRIVATE</ValEnum>
+               <ValEnum>CONFIDENTIAL</ValEnum>
+       <PropName>DAYLIGHT</PropName>
+       <PropName>DCREATED</PropName>
+       <PropName>DESCRIPTION</PropName>
+       <PropName>DTSTART</PropName>
+       <PropName>DTEND</PropName>
+       <PropName>DUE</PropName>
+       <PropName>END</PropName>
+               <ValEnum>VEVENT</ValEnum>
+               <ValEnum>VCALENDAR</ValEnum>
+               <ValEnum>VTODO</ValEnum>
+       <PropName>EXDATE</PropName>
+       <PropName>LAST-MODIFIED</PropName>
+       <PropName>LOCATION</PropName>
+       <PropName>PRIORITY</PropName>
+       <PropName>RRULE</PropName>
+       <PropName>STATUS</PropName>
+       <PropName>SUMMARY</PropName>
+       <PropName>UID</PropName>
+       <PropName>VERSION</PropName>
+               <ValEnum>1.0</ValEnum>
+</CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns='syncml:metinf'>application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf10</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source><Meta><Anchor xmlns='syncml:metinf'><Last>20061222T210325Z</Last><Next>20061222T210353Z</Next></Anchor></Meta></Item></Alert><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_61.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_61.xml
new file mode 100644 (file)
index 0000000..7e773d2
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>2</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns='syncml:metinf'><Next>1167761824</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>c:\Documents\agenda\agenda</LocURI></Source></Sync><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_62.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_client_62.xml
new file mode 100644 (file)
index 0000000..9a2dd13
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><SyncML xmlns='SYNCML:SYNCML1.0'><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>3</MsgID><Target><LocURI>http://example.com/rpc.php</LocURI></Target><Source><LocURI>351965-00-340413-3</LocURI></Source><Meta><MaxMsgSize xmlns='syncml:metinf'>200000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>351965-00-340413-3</TargetRef><SourceRef>http://example.com/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>c:\Documents\agenda\agenda</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Final/></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_log.txt b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_log.txt
new file mode 100644 (file)
index 0000000..ef70fa8
--- /dev/null
@@ -0,0 +1,269 @@
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml3519650034041331613468436
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  SyncML: No info about previous syncs found for device 351965-00-340413-3 and database calendar
+DEBUG:  Create new sync for calendar; synctype=201
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:06:36. Packet logged in /tmp/sync/syncml_server_11.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331613468436
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 667 suid 
+DEBUG:  No map entry found
+DEBUG:  add to server db calendar cuid 667 -> suid 20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+DEBUG:  added client entry due to replace request as 20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 678 suid 
+DEBUG:  No map entry found
+DEBUG:  add to server db calendar cuid 678 -> suid 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org
+DEBUG:  added client entry due to replace request as 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db tasks cuid 679 suid 
+DEBUG:  No map entry found
+DEBUG:  add to server db tasks cuid 679 -> suid 20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+DEBUG:  added client entry due to replace request as 20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  add: 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org ignored, came from client
+DEBUG:  add: 20070102190646.11w7isgi2ywg@fourmont.dyndns.org ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  add: 20070102190651.31xjl2phtjy8@fourmont.dyndns.org ignored, came from client
+DEBUG:  sending 3 server changes for syncType=calendar
+DEBUG:  add: 20070102190218.5fbc16hzzroc@voltaire.local
+DEBUG:  add: 20070102190234.3wz9alsndh0k@voltaire.local
+DEBUG:  add: 20070102183704.6l75i6x9a2gw@voltaire.local
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:06:55. Packet logged in /tmp/sync/syncml_server_12.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331613468436
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  created Map for cuid=680 and suid=20070102190218.5fbc16hzzroc@voltaire.local in db calendar
+DEBUG:  created Map for cuid=681 and suid=20070102190234.3wz9alsndh0k@voltaire.local in db calendar
+DEBUG:  created Map for cuid=682 and suid=20070102183704.6l75i6x9a2gw@voltaire.local in db tasks
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,3; server(Add,Replace,Delete)=3,0,0
+DEBUG:  Finished at 2007-01-02 19:07:03. Packet logged in /tmp/sync/syncml_server_20.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml351965003404133288553580
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 20061222T205323Z
+DEBUG:  SyncML: Anchor match, TwoWaySync since 20061222T205323Z
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:10:47. Packet logged in /tmp/sync/syncml_server_21.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133288553580
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 680 suid 20070102190218.5fbc16hzzroc@voltaire.local
+DEBUG:  replaced entry 20070102190218.5fbc16hzzroc@voltaire.local due to client request
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 681 suid 20070102190234.3wz9alsndh0k@voltaire.local
+DEBUG:  replaced entry 20070102190234.3wz9alsndh0k@voltaire.local due to client request
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db tasks cuid 682 suid 20070102183704.6l75i6x9a2gw@voltaire.local
+DEBUG:  replaced entry 20070102183704.6l75i6x9a2gw@voltaire.local due to client request
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  add: 20070102190646.11w7isgi2ywg@fourmont.dyndns.org ignored, came from client
+DEBUG:  add: 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org ignored, came from client
+DEBUG:  change: 20070102190218.5fbc16hzzroc@voltaire.local ignored, came from client
+DEBUG:  change: 20070102190234.3wz9alsndh0k@voltaire.local ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  add: 20070102190651.31xjl2phtjy8@fourmont.dyndns.org ignored, came from client
+DEBUG:  change: 20070102183704.6l75i6x9a2gw@voltaire.local ignored, came from client
+DEBUG:  sending 3 server changes for syncType=calendar
+DEBUG:  replace: 20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+DEBUG:  replace: 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org
+DEBUG:  replace: 20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:10:54. Packet logged in /tmp/sync/syncml_server_22.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133288553580
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,3,0,0; server(Add,Replace,Delete)=0,3,0
+DEBUG:  Finished at 2007-01-02 19:11:01. Packet logged in /tmp/sync/syncml_server_30.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml351965003404133539441908
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 20061222T205735Z
+DEBUG:  SyncML: Anchor match, TwoWaySync since 20061222T205735Z
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:12:04. Packet logged in /tmp/sync/syncml_server_31.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133539441908
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 667 suid 20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+DEBUG:  replaced entry 20070102190646.11w7isgi2ywg@fourmont.dyndns.org due to client request
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 678 suid 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org
+DEBUG:  replaced entry 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org due to client request
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db tasks cuid 679 suid 20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+DEBUG:  replaced entry 20070102190651.31xjl2phtjy8@fourmont.dyndns.org due to client request
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  change: 20070102190218.5fbc16hzzroc@voltaire.local ignored, came from client
+DEBUG:  change: 20070102190234.3wz9alsndh0k@voltaire.local ignored, came from client
+DEBUG:  change: 20070102190646.11w7isgi2ywg@fourmont.dyndns.org ignored, came from client
+DEBUG:  change: 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  change: 20070102183704.6l75i6x9a2gw@voltaire.local ignored, came from client
+DEBUG:  change: 20070102190651.31xjl2phtjy8@fourmont.dyndns.org ignored, came from client
+DEBUG:  sending 0 server changes for syncType=calendar
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:12:09. Packet logged in /tmp/sync/syncml_server_32.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133539441908
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,3,0,0; server(Add,Replace,Delete)=0,0,0
+DEBUG:  Finished at 2007-01-02 19:12:12. Packet logged in /tmp/sync/syncml_server_40.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml351965003404133890389626
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 20061222T205851Z
+DEBUG:  SyncML: Anchor match, TwoWaySync since 20061222T205851Z
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:14:13. Packet logged in /tmp/sync/syncml_server_41.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133890389626
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  change: 20070102190646.11w7isgi2ywg@fourmont.dyndns.org ignored, came from client
+DEBUG:  change: 20070102190649.6fhtdnkvlrsw@fourmont.dyndns.org ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  change: 20070102190651.31xjl2phtjy8@fourmont.dyndns.org ignored, came from client
+DEBUG:  sending 0 server changes for syncType=calendar
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:14:19. Packet logged in /tmp/sync/syncml_server_42.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml351965003404133890389626
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,0; server(Add,Replace,Delete)=0,0,0
+DEBUG:  Finished at 2007-01-02 19:14:22. Packet logged in /tmp/sync/syncml_server_50.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml3519650034041331432169629
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 20061222T210039Z
+DEBUG:  SyncML: Anchor match, TwoWaySync since 20061222T210039Z
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:16:37. Packet logged in /tmp/sync/syncml_server_51.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331432169629
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  Handling client sent Delete
+DEBUG:  deleted entry  due to client request
+DEBUG:  Handling client sent Delete
+DEBUG:  special tasks delete  due to client request
+DEBUG:  deleted entry  due to client request
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  SyncML: delete 20070102190234.3wz9alsndh0k@voltaire.local ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  SyncML: delete 20070102183704.6l75i6x9a2gw@voltaire.local ignored, came from client
+DEBUG:  sending 2 server changes for syncType=calendar
+DEBUG:  delete: cuid=667 suid=20070102190646.11w7isgi2ywg@fourmont.dyndns.org
+DEBUG:  delete: cuid=679 suid=20070102190651.31xjl2phtjy8@fourmont.dyndns.org
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:16:43. Packet logged in /tmp/sync/syncml_server_52.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331432169629
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,2,0; server(Add,Replace,Delete)=0,0,2
+DEBUG:  Finished at 2007-01-02 19:16:45. Packet logged in /tmp/sync/syncml_server_60.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncml3519650034041331523248674
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=0 msgid=1 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 20061222T210325Z
+DEBUG:  SyncML: Anchor match, TwoWaySync since 20061222T210325Z
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:17:04. Packet logged in /tmp/sync/syncml_server_61.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331523248674
+DEBUG:  authorized=1 version=0 msgid=2 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_P800
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes
+DEBUG:  SyncML: delete 20070102190234.3wz9alsndh0k@voltaire.local ignored, came from client
+DEBUG:  handling tasks in calendar sync
+DEBUG:  SyncML: delete 20070102183704.6l75i6x9a2gw@voltaire.local ignored, came from client
+DEBUG:  sending 0 server changes for syncType=calendar
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-02 19:17:09. Packet logged in /tmp/sync/syncml_server_62.(wb)xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncml3519650034041331523248674
+DEBUG:  authorized=1 version=0 msgid=3 source=351965-00-340413-3 target=http://example.com/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,0; server(Add,Replace,Delete)=0,0,0
+DEBUG:  Finished at 2007-01-02 19:17:12. Packet logged in /tmp/sync/syncml_server_70.(wb)xml
+
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_10.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..1ec4df0
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx-Pref><Tx-Pref><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx-Pref><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/directory</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/directory</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>508</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T204212Z</Last><Next>20061222T205323Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>201</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1167761196</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_11.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..238e150
--- /dev/null
@@ -0,0 +1,48 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Add><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>20070102190218.5fbc16hzzroc@voltaire.local</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+X-EPOCAGENDAENTRYTYPE:EVENT
+DTSTART:20070101T000000
+DTEND:20070102T000000
+DTSTAMP:20070102T180655Z
+SUMMARY:server1allday new year
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:mailto:syncmltest
+LOCATION:everywhere
+CLASS:PUBLIC
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Add><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>20070102190234.3wz9alsndh0k@voltaire.local</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+DTSTAMP:20070102T180655Z
+SUMMARY:server2 6-7
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:mailto:syncmltest
+CLASS:PUBLIC
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Add><Add><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>20070102183704.6l75i6x9a2gw@voltaire.local</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:servertask1
+PRIORITY:0
+STATUS:NEEDS ACTION
+DCREATED:20070610T134320Z
+LAST-MODIFIED:20070610T134320Z
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Add></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_12.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..d21e8e0
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1613468436</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>6</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>3</MsgRef><CmdRef>7</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>3</MsgRef><CmdRef>8</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_20.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..1e1022c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T205323Z</Last><Next>20061222T205735Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>200</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1167761196</Last><Next>1167761447</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_21.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..509e12d
--- /dev/null
@@ -0,0 +1,53 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Replace><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Target><LocURI>667</LocURI></Target><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20061222T110000Z
+DTEND:20061222T120000Z
+DTSTAMP:20070102T181054Z
+SUMMARY:client1s1 12-1
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:mailto:syncmltest
+CLASS:PUBLIC
+STATUS:CONFIRMED
+AALARM:20061222T105000Z
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Target><LocURI>678</LocURI></Target><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+X-EPOCAGENDAENTRYTYPE:EVENT
+DTSTART:20061225T000000
+DTEND:20061226T000000
+DTSTAMP:20070102T181054Z
+SUMMARY:cIient1s1 christmas all day
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:mailto:syncmltest
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+uumlaut: =C3=BC
+CLASS:PUBLIC
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Target><LocURI>679</LocURI></Target><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:clienttask1s1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+Aumlaut: =C3=84
+PRIORITY:0
+DTSTART:19700101T000001Z
+STATUS:NEEDS ACTION
+DCREATED:20070610T145225Z
+LAST-MODIFIED:20070610T145228Z
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_22.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..72f8d8d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>288553580</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_30.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..65aecdb
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T205735Z</Last><Next>20061222T205851Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>200</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1167761447</Last><Next>1167761524</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_31.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..fc994e5
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_32.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..a39f118
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>539441908</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_40.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..d4f8b34
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T205851Z</Last><Next>20061222T210039Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>200</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1167761524</Last><Next>1167761653</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_41.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..d4cbb03
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_42.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..6623c0e
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>890389626</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_50.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_50.xml
new file mode 100644 (file)
index 0000000..10fee55
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T210039Z</Last><Next>20061222T210325Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>200</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1167761653</Last><Next>1167761797</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_51.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_51.xml
new file mode 100644 (file)
index 0000000..7d2be5a
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Delete><CmdID>4</CmdID><Item><Target><LocURI>667</LocURI></Target></Item></Delete><Delete><CmdID>5</CmdID><Item><Target><LocURI>679</LocURI></Target></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_52.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_52.xml
new file mode 100644 (file)
index 0000000..bc95d33
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1432169629</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_60.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_60.xml
new file mode 100644 (file)
index 0000000..d4283bb
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>1</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf10</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf10</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf10</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.0</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>fourmont.dyndns.org</DevID><DevTyp>server</DevTyp><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20061222T210325Z</Last><Next>20061222T210353Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>200</Data><Item><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1167761797</Last><Next>1167761824</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_61.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_61.xml
new file mode 100644 (file)
index 0000000..d9b25ce
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>2</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>c:\Documents\agenda\agenda</SourceRef><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>c:\Documents\agenda\agenda</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_62.xml b/framework/SyncML/tests/testcase_P900_tasksandcalendar/syncml_server_62.xml
new file mode 100644 (file)
index 0000000..b459a8f
--- /dev/null
@@ -0,0 +1 @@
+<SyncML><SyncHdr><VerDTD>1.0</VerDTD><VerProto>SyncML/1.0</VerProto><SessionID>1523248674</SessionID><MsgID>3</MsgID><Target><LocURI>351965-00-340413-3</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://example.com/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://example.com/rpc.php</TargetRef><SourceRef>351965-00-340413-3</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/data.txt b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/data.txt
new file mode 100644 (file)
index 0000000..8761fd1
--- /dev/null
@@ -0,0 +1,202 @@
+
+input received from client (text/x-s4j-sife)
+<appointment><AllDayEvent>1</AllDayEvent><Start>2007-01-01</Start><End>2007-01-01</End><BillingInformation></BillingInformation><Body>Test Aumlaut: Ã„</Body><BusyStatus>0</BusyStatus><Categories></Categories><Companies></Companies><Importance>1</Importance><IsRecurring>0</IsRecurring><Location>everywhere</Location><MeetingStatus>0</MeetingStatus><Mileage></Mileage><ReminderMinutesBeforeStart>1080</ReminderMinutesBeforeStart><ReminderSet>0</ReminderSet><ReplyTime></ReplyTime><Sensitivity>0</Sensitivity><Subject>new year all day client1</Subject><RecurrenceType>1</RecurrenceType><Interval>1</Interval><MonthOfYear>0</MonthOfYear><DayOfMonth>0</DayOfMonth><DayOfWeekMask>2</DayOfWeekMask><Instance>0</Instance><PatternStartDate>20061231T230000Z</PatternStartDate><NoEndDate>1</NoEndDate><PatternEndDate></PatternEndDate><Occurrences></Occurrences></appointment>
+
+input converted for server (text/x-vevent):
+BEGIN:VEVENT
+DTSTAMP:20070105T213440Z
+SUMMARY:new year all day client1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+Test Aumlaut: =C3=84
+LOCATION:everywhere
+DTSTART:20070101T000000
+DTEND:20070101T235959
+STATUS:FREE
+CLASS:PUBLIC
+UID:20070105T213440Z2u5w9rfe2lq8@voltaire.local
+END:VEVENT
+
+
+input received from client (text/x-s4j-sife)
+<appointment><AllDayEvent>0</AllDayEvent><Start>20070107T220000Z</Start><End>20070107T223000Z</End><BillingInformation></BillingInformation><Body>Alarm 30minutes before</Body><BusyStatus>2</BusyStatus><Categories></Categories><Companies></Companies><Importance>1</Importance><IsRecurring>0</IsRecurring><Location></Location><MeetingStatus>0</MeetingStatus><Mileage></Mileage><ReminderMinutesBeforeStart>30</ReminderMinutesBeforeStart><ReminderSet>1</ReminderSet><ReplyTime></ReplyTime><Sensitivity>0</Sensitivity><Subject>Client2 23:00-23:30</Subject><RecurrenceType>1</RecurrenceType><Interval>1</Interval><MonthOfYear>0</MonthOfYear><DayOfMonth>0</DayOfMonth><DayOfWeekMask>1</DayOfWeekMask><Instance>0</Instance><PatternStartDate>20070106T230000Z</PatternStartDate><NoEndDate>1</NoEndDate><PatternEndDate></PatternEndDate><Occurrences></Occurrences></appointment>
+
+input converted for server (text/x-vevent):
+BEGIN:VEVENT
+DTSTAMP:20070105T213442Z
+SUMMARY:Client2 23:00-23:30
+DESCRIPTION:Alarm 30minutes before
+DTSTART:20070107T220000Z
+DTEND:20070107T223000Z
+AALARM:20070107T213000Z
+STATUS:CONFIRMED
+CLASS:PUBLIC
+UID:20070105T213442Z7ecv8peqmr48@voltaire.local
+END:VEVENT
+
+
+output received from horde backend (text/calendar):
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+DTSTAMP:20070105T213445Z
+UID:20070105223422.1ymx6b99mycg@voltaire.local
+SUMMARY:server2 6-7
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-s4j-sife):
+<?xml version="1.0"?><appointment><AllDayEvent>0</AllDayEvent><BusyStatus>2</BusyStatus><Duration>60</Duration><End>20070102T060000Z</End><IsRecurring>0</IsRecurring><ReminderSet>0</ReminderSet><Sensitivity>0</Sensitivity><Start>20070102T050000Z</Start><Subject>server2 6-7</Subject></appointment>
+
+output received from horde backend (text/calendar):
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070106T050000Z
+DTEND:20070106T060000Z
+DTSTAMP:20070105T213445Z
+UID:20070105223408.6wew7kh5jag4@voltaire.local
+SUMMARY:all day three kings server1
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+DESCRIPTION:test uumlaut\: Ã¼
+CATEGORIES:holiday
+LOCATION:almost everywhere
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-s4j-sife):
+<?xml version="1.0"?><appointment><AllDayEvent>0</AllDayEvent><Body>test uumlaut: Ã¼</Body><BusyStatus>2</BusyStatus><Categories>holiday</Categories><Duration>60</Duration><End>20070106T060000Z</End><IsRecurring>0</IsRecurring><Location>almost everywhere</Location><ReminderSet>0</ReminderSet><Sensitivity>0</Sensitivity><Start>20070106T050000Z</Start><Subject>all day three kings server1</Subject></appointment>
+
+input received from client (text/x-s4j-sife)
+<appointment><AllDayEvent>0</AllDayEvent><Start>20070105T220000Z</Start><End>20070105T223000Z</End><BillingInformation></BillingInformation><Body></Body><BusyStatus>2</BusyStatus><Categories></Categories><Companies></Companies><Importance>1</Importance><IsRecurring>0</IsRecurring><Location></Location><MeetingStatus>0</MeetingStatus><Mileage></Mileage><ReminderMinutesBeforeStart>15</ReminderMinutesBeforeStart><ReminderSet>1</ReminderSet><ReplyTime></ReplyTime><Sensitivity>0</Sensitivity><Subject>client99</Subject><RecurrenceType>1</RecurrenceType><Interval>1</Interval><MonthOfYear>0</MonthOfYear><DayOfMonth>0</DayOfMonth><DayOfWeekMask>32</DayOfWeekMask><Instance>0</Instance><PatternStartDate>20070104T230000Z</PatternStartDate><NoEndDate>1</NoEndDate><PatternEndDate></PatternEndDate><Occurrences></Occurrences></appointment>
+
+input converted for server (text/x-vevent):
+BEGIN:VEVENT
+DTSTAMP:20070105T213608Z
+SUMMARY:client99
+DTSTART:20070105T220000Z
+DTEND:20070105T223000Z
+AALARM:20070105T214500Z
+STATUS:CONFIRMED
+CLASS:PUBLIC
+UID:20070105T213608Z97fh32byy88@voltaire.local
+END:VEVENT
+
+
+input received from client (text/x-s4j-sife)
+<appointment><AllDayEvent>0</AllDayEvent><Start>20070102T050000Z</Start><End>20070102T060000Z</End><BillingInformation></BillingInformation><Body></Body><BusyStatus>2</BusyStatus><Categories></Categories><Companies></Companies><Importance>0</Importance><IsRecurring>0</IsRecurring><Location></Location><MeetingStatus>0</MeetingStatus><Mileage></Mileage><ReminderMinutesBeforeStart>0</ReminderMinutesBeforeStart><ReminderSet>0</ReminderSet><ReplyTime></ReplyTime><Sensitivity>0</Sensitivity><Subject>server2c2 6-7</Subject><RecurrenceType>1</RecurrenceType><Interval>1</Interval><MonthOfYear>0</MonthOfYear><DayOfMonth>0</DayOfMonth><DayOfWeekMask>4</DayOfWeekMask><Instance>0</Instance><PatternStartDate>20070101T230000Z</PatternStartDate><NoEndDate>1</NoEndDate><PatternEndDate></PatternEndDate><Occurrences></Occurrences></appointment>
+
+input converted for server (text/x-vevent):
+BEGIN:VEVENT
+DTSTAMP:20070105T213608Z
+SUMMARY:server2c2 6-7
+DTSTART:20070102T050000Z
+DTEND:20070102T060000Z
+STATUS:CONFIRMED
+CLASS:PUBLIC
+UID:20070105T213608Z5ojuhybwlds0@voltaire.local
+END:VEVENT
+
+
+input received from client (text/x-s4j-sife)
+<appointment><AllDayEvent>0</AllDayEvent><Start>20070106T050000Z</Start><End>20070106T060000Z</End><BillingInformation></BillingInformation><Body>test uumlaut: Ã¼</Body><BusyStatus>2</BusyStatus><Categories>holiday</Categories><Companies></Companies><Importance>0</Importance><IsRecurring>0</IsRecurring><Location>almost everywhere</Location><MeetingStatus>0</MeetingStatus><Mileage></Mileage><ReminderMinutesBeforeStart>0</ReminderMinutesBeforeStart><ReminderSet>0</ReminderSet><ReplyTime></ReplyTime><Sensitivity>0</Sensitivity><Subject>all day three kings server1c1</Subject><RecurrenceType>1</RecurrenceType><Interval>1</Interval><MonthOfYear>0</MonthOfYear><DayOfMonth>0</DayOfMonth><DayOfWeekMask>64</DayOfWeekMask><Instance>0</Instance><PatternStartDate>20070105T230000Z</PatternStartDate><NoEndDate>1</NoEndDate><PatternEndDate></PatternEndDate><Occurrences></Occurrences></appointment>
+
+input converted for server (text/x-vevent):
+BEGIN:VEVENT
+DTSTAMP:20070105T213609Z
+SUMMARY:all day three kings server1c1
+DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=
+test uumlaut: =C3=BC
+CATEGORIES:holiday
+LOCATION:almost everywhere
+DTSTART:20070106T050000Z
+DTEND:20070106T060000Z
+STATUS:CONFIRMED
+CLASS:PUBLIC
+UID:20070105T213609Zg9gt7fvplnw@voltaire.local
+END:VEVENT
+
+
+output received from horde backend (text/calendar):
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070103T050000Z
+DTEND:20070103T060000Z
+DTSTAMP:20070105T213610Z
+UID:20070105223456.5x0i61i3nbgo@voltaire.local
+SUMMARY:server99
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-s4j-sife):
+<?xml version="1.0"?><appointment><AllDayEvent>0</AllDayEvent><BusyStatus>2</BusyStatus><Duration>60</Duration><End>20070103T060000Z</End><IsRecurring>0</IsRecurring><ReminderSet>0</ReminderSet><Sensitivity>0</Sensitivity><Start>20070103T050000Z</Start><Subject>server99</Subject></appointment>
+
+output received from horde backend (text/calendar):
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20070101
+DTEND;VALUE=DATE:20070101
+DTSTAMP:20070105T213610Z
+UID:20070105T213440Z2u5w9rfe2lq8@voltaire.local
+SUMMARY:new year all day client1s1
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+DESCRIPTION:Test Aumlaut\: Ã„
+LOCATION:everywhere
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-s4j-sife):
+<?xml version="1.0"?><appointment><AllDayEvent>1</AllDayEvent><Body>Test Aumlaut: Ã„</Body><BusyStatus>2</BusyStatus><End>2007-01-01</End><IsRecurring>0</IsRecurring><Location>everywhere</Location><ReminderSet>0</ReminderSet><Sensitivity>0</Sensitivity><Start>2007-01-01</Start><Subject>new year all day client1s1</Subject></appointment>
+
+output received from horde backend (text/calendar):
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:syncmltest's Calendar
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+DTSTART:20070107T220000Z
+DTEND:20070107T223000Z
+DTSTAMP:20070105T213610Z
+UID:20070105T213442Z7ecv8peqmr48@voltaire.local
+SUMMARY:Client2s2 23\:00-23\:30
+TRANSP:OPAQUE
+ORGANIZER;CN=syncmltest:MAILTO:syncmltest
+DESCRIPTION:Alarm 30minutes before
+CLASS:PUBLIC
+AALARM:20070107T213000Z
+END:VEVENT
+END:VCALENDAR
+
+
+output converted for client (text/x-s4j-sife):
+<?xml version="1.0"?><appointment><AllDayEvent>0</AllDayEvent><Body>Alarm 30minutes before</Body><BusyStatus>2</BusyStatus><Duration>30</Duration><End>20070107T223000Z</End><IsRecurring>0</IsRecurring><ReminderMinutesBeforeStart>30</ReminderMinutesBeforeStart><ReminderSet>1</ReminderSet><Sensitivity>0</Sensitivity><Start>20070107T220000Z</Start><Subject>Client2s2 23:00-23:30</Subject></appointment>
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_10.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..eebdf12
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032875</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>201</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>0</Last>
+<Next>1168032875</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_11.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..663d2ca
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032875</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1168032879</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Replace><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50049C2000</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4xPC9BbGxEYXlFdmVudD48U3RhcnQ+MjAwNy0wMS0wMTwvU3RhcnQ+PEVuZD4yMDA3LTAxLTAxPC9FbmQ+PEJpbGxpbmdJbmZvcm1hdGlvbj48L0JpbGxpbmdJbmZvcm1hdGlvbj48Qm9keT5UZXN0IEF1bWxhdXQ6IMOEPC9Cb2R5PjxCdXN5U3RhdHVzPjA8L0J1c3lTdGF0dXM+PENhdGVnb3JpZXM+PC9DYXRlZ29yaWVzPjxDb21wYW5pZXM+PC9Db21wYW5pZXM+PEltcG9ydGFuY2U+MTwvSW1wb3J0YW5jZT48SXNSZWN1cnJpbmc+MDwvSXNSZWN1cnJpbmc+PExvY2F0aW9uPmV2ZXJ5d2hlcmU8L0xvY2F0aW9uPjxNZWV0aW5nU3RhdHVzPjA8L01lZXRpbmdTdGF0dXM+PE1pbGVhZ2U+PC9NaWxlYWdlPjxSZW1pbmRlck1pbnV0ZXNCZWZvcmVTdGFydD4xMDgwPC9SZW1pbmRlck1pbnV0ZXNCZWZvcmVTdGFydD48UmVtaW5kZXJTZXQ+MDwvUmVtaW5kZXJTZXQ+PFJlcGx5VGltZT48L1JlcGx5VGltZT48U2Vuc2l0aXZpdHk+MDwvU2Vuc2l0aXZpdHk+PFN1YmplY3Q+bmV3IHllYXIgYWxsIGRheSBjbGllbnQxPC9TdWJqZWN0PjxSZWN1cnJlbmNlVHlwZT4xPC9SZWN1cnJlbmNlVHlwZT48SW50ZXJ2YWw+MTwvSW50ZXJ2YWw+PE1vbnRoT2ZZZWFyPjA8L01vbnRoT2ZZZWFyPjxEYXlPZk1vbnRoPjA8L0RheU9mTW9udGg+PERheU9mV2Vla01hc2s+MjwvRGF5T2ZXZWVrTWFzaz48SW5zdGFuY2U+MDwvSW5zdGFuY2U+PFBhdHRlcm5TdGFydERhdGU+MjAwNjEyMzFUMjMwMDAwWjwvUGF0dGVyblN0YXJ0RGF0ZT48Tm9FbmREYXRlPjE8L05vRW5kRGF0ZT48UGF0dGVybkVuZERhdGU+PC9QYXR0ZXJuRW5kRGF0ZT48T2NjdXJyZW5jZXM+PC9PY2N1cnJlbmNlcz48L2FwcG9pbnRtZW50Pg==</Data>
+</Item>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50249C2000</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48U3RhcnQ+MjAwNzAxMDdUMjIwMDAwWjwvU3RhcnQ+PEVuZD4yMDA3MDEwN1QyMjMwMDBaPC9FbmQ+PEJpbGxpbmdJbmZvcm1hdGlvbj48L0JpbGxpbmdJbmZvcm1hdGlvbj48Qm9keT5BbGFybSAzMG1pbnV0ZXMgYmVmb3JlPC9Cb2R5PjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PENhdGVnb3JpZXM+PC9DYXRlZ29yaWVzPjxDb21wYW5pZXM+PC9Db21wYW5pZXM+PEltcG9ydGFuY2U+MTwvSW1wb3J0YW5jZT48SXNSZWN1cnJpbmc+MDwvSXNSZWN1cnJpbmc+PExvY2F0aW9uPjwvTG9jYXRpb24+PE1lZXRpbmdTdGF0dXM+MDwvTWVldGluZ1N0YXR1cz48TWlsZWFnZT48L01pbGVhZ2U+PFJlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjMwPC9SZW1pbmRlck1pbnV0ZXNCZWZvcmVTdGFydD48UmVtaW5kZXJTZXQ+MTwvUmVtaW5kZXJTZXQ+PFJlcGx5VGltZT48L1JlcGx5VGltZT48U2Vuc2l0aXZpdHk+MDwvU2Vuc2l0aXZpdHk+PFN1YmplY3Q+Q2xpZW50MiAyMzowMC0yMzozMDwvU3ViamVjdD48UmVjdXJyZW5jZVR5cGU+MTwvUmVjdXJyZW5jZVR5cGU+PEludGVydmFsPjE8L0ludGVydmFsPjxNb250aE9mWWVhcj4wPC9Nb250aE9mWWVhcj48RGF5T2ZNb250aD4wPC9EYXlPZk1vbnRoPjxEYXlPZldlZWtNYXNrPjE8L0RheU9mV2Vla01hc2s+PEluc3RhbmNlPjA8L0luc3RhbmNlPjxQYXR0ZXJuU3RhcnREYXRlPjIwMDcwMTA2VDIzMDAwMFo8L1BhdHRlcm5TdGFydERhdGU+PE5vRW5kRGF0ZT4xPC9Ob0VuZERhdGU+PFBhdHRlcm5FbmREYXRlPjwvUGF0dGVybkVuZERhdGU+PE9jY3VycmVuY2VzPjwvT2NjdXJyZW5jZXM+PC9hcHBvaW50bWVudD4=</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_12.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..9c0eb28
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032875</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_13.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_13.xml
new file mode 100644 (file)
index 0000000..9d1fecc
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032875</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20070105223422.1ymx6b99mycg@voltaire.local</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>4</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20070105223408.6wew7kh5jag4@voltaire.local</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>5</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<MapItem><Target><LocURI>20070105223422.1ymx6b99mycg@voltaire.local</LocURI>
+</Target>
+<Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50449C2000</LocURI>
+</Source>
+</MapItem>
+<MapItem><Target><LocURI>20070105223408.6wew7kh5jag4@voltaire.local</LocURI>
+</Target>
+<Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50649C2000</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_20.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..9f85c09
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032964</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>1168032875</Last>
+<Next>1168032964</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_21.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..01741ec
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032964</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1168032967</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Add><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50849C2000</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48U3RhcnQ+MjAwNzAxMDVUMjIwMDAwWjwvU3RhcnQ+PEVuZD4yMDA3MDEwNVQyMjMwMDBaPC9FbmQ+PEJpbGxpbmdJbmZvcm1hdGlvbj48L0JpbGxpbmdJbmZvcm1hdGlvbj48Qm9keT48L0JvZHk+PEJ1c3lTdGF0dXM+MjwvQnVzeVN0YXR1cz48Q2F0ZWdvcmllcz48L0NhdGVnb3JpZXM+PENvbXBhbmllcz48L0NvbXBhbmllcz48SW1wb3J0YW5jZT4xPC9JbXBvcnRhbmNlPjxJc1JlY3VycmluZz4wPC9Jc1JlY3VycmluZz48TG9jYXRpb24+PC9Mb2NhdGlvbj48TWVldGluZ1N0YXR1cz4wPC9NZWV0aW5nU3RhdHVzPjxNaWxlYWdlPjwvTWlsZWFnZT48UmVtaW5kZXJNaW51dGVzQmVmb3JlU3RhcnQ+MTU8L1JlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjxSZW1pbmRlclNldD4xPC9SZW1pbmRlclNldD48UmVwbHlUaW1lPjwvUmVwbHlUaW1lPjxTZW5zaXRpdml0eT4wPC9TZW5zaXRpdml0eT48U3ViamVjdD5jbGllbnQ5OTwvU3ViamVjdD48UmVjdXJyZW5jZVR5cGU+MTwvUmVjdXJyZW5jZVR5cGU+PEludGVydmFsPjE8L0ludGVydmFsPjxNb250aE9mWWVhcj4wPC9Nb250aE9mWWVhcj48RGF5T2ZNb250aD4wPC9EYXlPZk1vbnRoPjxEYXlPZldlZWtNYXNrPjMyPC9EYXlPZldlZWtNYXNrPjxJbnN0YW5jZT4wPC9JbnN0YW5jZT48UGF0dGVyblN0YXJ0RGF0ZT4yMDA3MDEwNFQyMzAwMDBaPC9QYXR0ZXJuU3RhcnREYXRlPjxOb0VuZERhdGU+MTwvTm9FbmREYXRlPjxQYXR0ZXJuRW5kRGF0ZT48L1BhdHRlcm5FbmREYXRlPjxPY2N1cnJlbmNlcz48L09jY3VycmVuY2VzPjwvYXBwb2ludG1lbnQ+</Data>
+</Item>
+</Add>
+<Replace><CmdID>5</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50449C2000</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48U3RhcnQ+MjAwNzAxMDJUMDUwMDAwWjwvU3RhcnQ+PEVuZD4yMDA3MDEwMlQwNjAwMDBaPC9FbmQ+PEJpbGxpbmdJbmZvcm1hdGlvbj48L0JpbGxpbmdJbmZvcm1hdGlvbj48Qm9keT48L0JvZHk+PEJ1c3lTdGF0dXM+MjwvQnVzeVN0YXR1cz48Q2F0ZWdvcmllcz48L0NhdGVnb3JpZXM+PENvbXBhbmllcz48L0NvbXBhbmllcz48SW1wb3J0YW5jZT4wPC9JbXBvcnRhbmNlPjxJc1JlY3VycmluZz4wPC9Jc1JlY3VycmluZz48TG9jYXRpb24+PC9Mb2NhdGlvbj48TWVldGluZ1N0YXR1cz4wPC9NZWV0aW5nU3RhdHVzPjxNaWxlYWdlPjwvTWlsZWFnZT48UmVtaW5kZXJNaW51dGVzQmVmb3JlU3RhcnQ+MDwvUmVtaW5kZXJNaW51dGVzQmVmb3JlU3RhcnQ+PFJlbWluZGVyU2V0PjA8L1JlbWluZGVyU2V0PjxSZXBseVRpbWU+PC9SZXBseVRpbWU+PFNlbnNpdGl2aXR5PjA8L1NlbnNpdGl2aXR5PjxTdWJqZWN0PnNlcnZlcjJjMiA2LTc8L1N1YmplY3Q+PFJlY3VycmVuY2VUeXBlPjE8L1JlY3VycmVuY2VUeXBlPjxJbnRlcnZhbD4xPC9JbnRlcnZhbD48TW9udGhPZlllYXI+MDwvTW9udGhPZlllYXI+PERheU9mTW9udGg+MDwvRGF5T2ZNb250aD48RGF5T2ZXZWVrTWFzaz40PC9EYXlPZldlZWtNYXNrPjxJbnN0YW5jZT4wPC9JbnN0YW5jZT48UGF0dGVyblN0YXJ0RGF0ZT4yMDA3MDEwMVQyMzAwMDBaPC9QYXR0ZXJuU3RhcnREYXRlPjxOb0VuZERhdGU+MTwvTm9FbmREYXRlPjxQYXR0ZXJuRW5kRGF0ZT48L1BhdHRlcm5FbmREYXRlPjxPY2N1cnJlbmNlcz48L09jY3VycmVuY2VzPjwvYXBwb2ludG1lbnQ+</Data>
+</Item>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50649C2000</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48U3RhcnQ+MjAwNzAxMDZUMDUwMDAwWjwvU3RhcnQ+PEVuZD4yMDA3MDEwNlQwNjAwMDBaPC9FbmQ+PEJpbGxpbmdJbmZvcm1hdGlvbj48L0JpbGxpbmdJbmZvcm1hdGlvbj48Qm9keT50ZXN0IHV1bWxhdXQ6IMO8PC9Cb2R5PjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PENhdGVnb3JpZXM+aG9saWRheTwvQ2F0ZWdvcmllcz48Q29tcGFuaWVzPjwvQ29tcGFuaWVzPjxJbXBvcnRhbmNlPjA8L0ltcG9ydGFuY2U+PElzUmVjdXJyaW5nPjA8L0lzUmVjdXJyaW5nPjxMb2NhdGlvbj5hbG1vc3QgZXZlcnl3aGVyZTwvTG9jYXRpb24+PE1lZXRpbmdTdGF0dXM+MDwvTWVldGluZ1N0YXR1cz48TWlsZWFnZT48L01pbGVhZ2U+PFJlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjA8L1JlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjxSZW1pbmRlclNldD4wPC9SZW1pbmRlclNldD48UmVwbHlUaW1lPjwvUmVwbHlUaW1lPjxTZW5zaXRpdml0eT4wPC9TZW5zaXRpdml0eT48U3ViamVjdD5hbGwgZGF5IHRocmVlIGtpbmdzIHNlcnZlcjFjMTwvU3ViamVjdD48UmVjdXJyZW5jZVR5cGU+MTwvUmVjdXJyZW5jZVR5cGU+PEludGVydmFsPjE8L0ludGVydmFsPjxNb250aE9mWWVhcj4wPC9Nb250aE9mWWVhcj48RGF5T2ZNb250aD4wPC9EYXlPZk1vbnRoPjxEYXlPZldlZWtNYXNrPjY0PC9EYXlPZldlZWtNYXNrPjxJbnN0YW5jZT4wPC9JbnN0YW5jZT48UGF0dGVyblN0YXJ0RGF0ZT4yMDA3MDEwNVQyMzAwMDBaPC9QYXR0ZXJuU3RhcnREYXRlPjxOb0VuZERhdGU+MTwvTm9FbmREYXRlPjxQYXR0ZXJuRW5kRGF0ZT48L1BhdHRlcm5FbmREYXRlPjxPY2N1cnJlbmNlcz48L09jY3VycmVuY2VzPjwvYXBwb2ludG1lbnQ+</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_22.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..0aa242a
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032964</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_23.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_23.xml
new file mode 100644 (file)
index 0000000..29448d3
--- /dev/null
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032964</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20070105223456.5x0i61i3nbgo@voltaire.local</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>4</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Replace</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50049C2000</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>5</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>6</CmdRef>
+<Cmd>Replace</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50249C2000</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>6</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<MapItem><Target><LocURI>20070105223456.5x0i61i3nbgo@voltaire.local</LocURI>
+</Target>
+<Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50A49C2000</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_30.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..92f65c7
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032979</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>1168032964</Last>
+<Next>1168032979</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_31.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..13d7280
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032979</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1168032981</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_32.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..552385c
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032979</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_33.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_33.xml
new file mode 100644 (file)
index 0000000..3a790c9
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168032979</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_40.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..a6361f6
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033004</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>1168032979</Last>
+<Next>1168033004</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_41.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..ecd64be
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033004</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1168033007</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Delete><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50A49C2000</LocURI>
+</Source>
+</Item>
+</Delete>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_42.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..c110962
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033004</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_43.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_43.xml
new file mode 100644 (file)
index 0000000..78626a8
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033004</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Delete</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50849C2000</LocURI>
+</Source>
+</Item>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_50.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_50.xml
new file mode 100644 (file)
index 0000000..49fc61d
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033013</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>1168033004</Last>
+<Next>1168033013</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_51.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_51.xml
new file mode 100644 (file)
index 0000000..79ed336
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033013</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1168033015</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_52.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_52.xml
new file mode 100644 (file)
index 0000000..8e403a0
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033013</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_53.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_client_53.xml
new file mode 100644 (file)
index 0000000..6043823
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>1168033013</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>sc-pim-outlook</LocURI>
+</Source>
+<Meta><MaxMsgSize>250000</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://voltaire.local/horde/rpc.php</TargetRef>
+<SourceRef>sc-pim-outlook</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_log.txt b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_log.txt
new file mode 100644 (file)
index 0000000..b9d15ba
--- /dev/null
@@ -0,0 +1,311 @@
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncmlscpimoutlook1168032875
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=1 msgid=1 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Create new sync for calendar; synctype=201
+DEBUG:  HandleFinal for state=0
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:34:39. Packet logged in /tmp/sync/syncml_server_10.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032875
+DEBUG:  authorized=1 version=1 msgid=2 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50049C2000 suid 
+DEBUG:  No map entry found
+DEBUG:  add to server db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50049C2000 -> suid 20070105T213440Z2u5w9rfe2lq8@voltaire.local
+DEBUG:  added client entry due to replace request as 20070105T213440Z2u5w9rfe2lq8@voltaire.local
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50249C2000 suid 
+DEBUG:  No map entry found
+DEBUG:  add to server db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50249C2000 -> suid 20070105T213442Z7ecv8peqmr48@voltaire.local
+DEBUG:  added client entry due to replace request as 20070105T213442Z7ecv8peqmr48@voltaire.local
+DEBUG:  HandleFinal for state=1
+DEBUG:  Not yet sending server sync data: special Funambol handling.
+DEBUG:  Received Final from client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:34:44. Packet logged in /tmp/sync/syncml_server_11.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032875
+DEBUG:  authorized=1 version=1 msgid=3 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes from 1970-01-01 01:00:00 to 2007-01-05 22:34:39
+DEBUG:  add: 20070105223422.1ymx6b99mycg@voltaire.local with add-ts=2007-01-05 22:34:22
+DEBUG:  add: 20070105T213440Z2u5w9rfe2lq8@voltaire.local ignored, came from client
+DEBUG:  add: 20070105T213442Z7ecv8peqmr48@voltaire.local ignored, came from client
+DEBUG:  add: 20070105223408.6wew7kh5jag4@voltaire.local with add-ts=2007-01-05 22:34:08
+DEBUG:  sending 2 server changes for syncType=calendar
+DEBUG:  add: 20070105223422.1ymx6b99mycg@voltaire.local
+DEBUG:  Sync4j for name=DTSTART, value=1167714000
+DEBUG:  Sync4j for name=DTEND, value=1167717600
+DEBUG:  Sync4j for name=DTSTAMP, value=1168032885
+DEBUG:  Sync4j for name=SUMMARY, value=server2 6-7
+DEBUG:  Sync4j for name=TRANSP, value=OPAQUE
+DEBUG:  Sync4j for name=ORGANIZER, value=MAILTO:syncmltest
+DEBUG:  Sync4j for name=CLASS, value=PUBLIC
+DEBUG:  add: 20070105223408.6wew7kh5jag4@voltaire.local
+DEBUG:  Sync4j for name=DTSTART, value=1168059600
+DEBUG:  Sync4j for name=DTEND, value=1168063200
+DEBUG:  Sync4j for name=DTSTAMP, value=1168032885
+DEBUG:  Sync4j for name=SUMMARY, value=all day three kings server1
+DEBUG:  Sync4j for name=TRANSP, value=OPAQUE
+DEBUG:  Sync4j for name=ORGANIZER, value=MAILTO:syncmltest
+DEBUG:  Sync4j for name=DESCRIPTION, value=test uumlaut: Ã¼
+DEBUG:  Sync4j for name=CATEGORIES, value=holiday
+DEBUG:  Sync4j for name=LOCATION, value=almost everywhere
+DEBUG:  Sync4j for name=CLASS, value=PUBLIC
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:34:45. Packet logged in /tmp/sync/syncml_server_12.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032875
+DEBUG:  authorized=1 version=1 msgid=4 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  created Map for cuid=000000004FCBE97B738E984EAF085560B1DD2D50449C2000 and suid=20070105223422.1ymx6b99mycg@voltaire.local in db calendar
+DEBUG:  created Map for cuid=000000004FCBE97B738E984EAF085560B1DD2D50649C2000 and suid=20070105223408.6wew7kh5jag4@voltaire.local in db calendar
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,2; server(Add,Replace,Delete)=2,0,0
+DEBUG:  Finished at 2007-01-05 22:34:46. Packet logged in /tmp/sync/syncml_server_13.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncmlscpimoutlook1168032964
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=1 msgid=1 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 1168032875
+DEBUG:  SyncML: Anchor match, TwoWaySync since 1168032875
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  HandleFinal for state=0
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:07. Packet logged in /tmp/sync/syncml_server_20.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032964
+DEBUG:  authorized=1 version=1 msgid=2 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  Handling client sent Add
+DEBUG:  add to server db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50849C2000 -> suid 20070105T213608Z97fh32byy88@voltaire.local
+DEBUG:  added client entry as 20070105T213608Z97fh32byy88@voltaire.local
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50449C2000 suid 20070105223422.1ymx6b99mycg@voltaire.local
+DEBUG:  replaced entry 20070105223422.1ymx6b99mycg@voltaire.local due to client request
+DEBUG:  Handling client sent Replace
+DEBUG:  replace in db calendar cuid 000000004FCBE97B738E984EAF085560B1DD2D50649C2000 suid 20070105223408.6wew7kh5jag4@voltaire.local
+DEBUG:  replaced entry 20070105223408.6wew7kh5jag4@voltaire.local due to client request
+DEBUG:  HandleFinal for state=1
+DEBUG:  Not yet sending server sync data: special Funambol handling.
+DEBUG:  Received Final from client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:09. Packet logged in /tmp/sync/syncml_server_21.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032964
+DEBUG:  authorized=1 version=1 msgid=3 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes from 2007-01-05 22:34:39 to 2007-01-05 22:36:07
+DEBUG:  add: 20070105223456.5x0i61i3nbgo@voltaire.local with add-ts=2007-01-05 22:34:56
+DEBUG:  add: 20070105T213440Z2u5w9rfe2lq8@voltaire.local ignored, came from client
+DEBUG:  add: 20070105T213442Z7ecv8peqmr48@voltaire.local ignored, came from client
+DEBUG:  add: 20070105T213608Z97fh32byy88@voltaire.local ignored, came from client
+DEBUG:  change: 20070105223408.6wew7kh5jag4@voltaire.local ignored, came from client
+DEBUG:  change: 20070105223422.1ymx6b99mycg@voltaire.local ignored, came from client
+DEBUG:  sending 3 server changes for syncType=calendar
+DEBUG:  add: 20070105223456.5x0i61i3nbgo@voltaire.local
+DEBUG:  Sync4j for name=DTSTART, value=1167800400
+DEBUG:  Sync4j for name=DTEND, value=1167804000
+DEBUG:  Sync4j for name=DTSTAMP, value=1168032970
+DEBUG:  Sync4j for name=SUMMARY, value=server99
+DEBUG:  Sync4j for name=TRANSP, value=OPAQUE
+DEBUG:  Sync4j for name=ORGANIZER, value=MAILTO:syncmltest
+DEBUG:  Sync4j for name=CLASS, value=PUBLIC
+DEBUG:  replace: 20070105T213440Z2u5w9rfe2lq8@voltaire.local
+DEBUG:  Sync4j for name=DTSTART, value=Array
+DEBUG:  Sync4j for name=DTEND, value=Array
+DEBUG:  Sync4j for name=DTSTAMP, value=1168032970
+DEBUG:  Sync4j for name=SUMMARY, value=new year all day client1s1
+DEBUG:  Sync4j for name=TRANSP, value=OPAQUE
+DEBUG:  Sync4j for name=ORGANIZER, value=MAILTO:syncmltest
+DEBUG:  Sync4j for name=DESCRIPTION, value=Test Aumlaut: Ã„
+DEBUG:  Sync4j for name=LOCATION, value=everywhere
+DEBUG:  Sync4j for name=CLASS, value=PUBLIC
+DEBUG:  replace: 20070105T213442Z7ecv8peqmr48@voltaire.local
+DEBUG:  Sync4j for name=DTSTART, value=1168207200
+DEBUG:  Sync4j for name=DTEND, value=1168209000
+DEBUG:  Sync4j for name=DTSTAMP, value=1168032970
+DEBUG:  Sync4j for name=SUMMARY, value=Client2s2 23:00-23:30
+DEBUG:  Sync4j for name=TRANSP, value=OPAQUE
+DEBUG:  Sync4j for name=ORGANIZER, value=MAILTO:syncmltest
+DEBUG:  Sync4j for name=DESCRIPTION, value=Alarm 30minutes before
+DEBUG:  Sync4j for name=CLASS, value=PUBLIC
+DEBUG:  Sync4j for name=AALARM, value=1168205400
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:10. Packet logged in /tmp/sync/syncml_server_22.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032964
+DEBUG:  authorized=1 version=1 msgid=4 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  created Map for cuid=000000004FCBE97B738E984EAF085560B1DD2D50A49C2000 and suid=20070105223456.5x0i61i3nbgo@voltaire.local in db calendar
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=1,2,0,0; server(Add,Replace,Delete)=1,2,0
+DEBUG:  Finished at 2007-01-05 22:36:11. Packet logged in /tmp/sync/syncml_server_23.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncmlscpimoutlook1168032979
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=1 msgid=1 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 1168032964
+DEBUG:  SyncML: Anchor match, TwoWaySync since 1168032964
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  HandleFinal for state=0
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:21. Packet logged in /tmp/sync/syncml_server_30.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032979
+DEBUG:  authorized=1 version=1 msgid=2 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  HandleFinal for state=1
+DEBUG:  Not yet sending server sync data: special Funambol handling.
+DEBUG:  Received Final from client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:21. Packet logged in /tmp/sync/syncml_server_31.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032979
+DEBUG:  authorized=1 version=1 msgid=3 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes from 2007-01-05 22:36:07 to 2007-01-05 22:36:21
+DEBUG:  add: 20070105T213608Z97fh32byy88@voltaire.local ignored, came from client
+DEBUG:  change: 20070105223422.1ymx6b99mycg@voltaire.local ignored, came from client
+DEBUG:  change: 20070105223408.6wew7kh5jag4@voltaire.local ignored, came from client
+DEBUG:  sending 0 server changes for syncType=calendar
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:22. Packet logged in /tmp/sync/syncml_server_32.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168032979
+DEBUG:  authorized=1 version=1 msgid=4 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,0; server(Add,Replace,Delete)=0,0,0
+DEBUG:  Finished at 2007-01-05 22:36:22. Packet logged in /tmp/sync/syncml_server_33.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncmlscpimoutlook1168033004
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=1 msgid=1 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 1168032979
+DEBUG:  SyncML: Anchor match, TwoWaySync since 1168032979
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  HandleFinal for state=0
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:47. Packet logged in /tmp/sync/syncml_server_40.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033004
+DEBUG:  authorized=1 version=1 msgid=2 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  Handling client sent Delete
+DEBUG:  deleted entry  due to client request
+DEBUG:  HandleFinal for state=1
+DEBUG:  Not yet sending server sync data: special Funambol handling.
+DEBUG:  Received Final from client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:48. Packet logged in /tmp/sync/syncml_server_41.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033004
+DEBUG:  authorized=1 version=1 msgid=3 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes from 2007-01-05 22:36:21 to 2007-01-05 22:36:47
+DEBUG:  SyncML: delete 20070105223456.5x0i61i3nbgo@voltaire.local ignored, came from client
+DEBUG:  sending 1 server changes for syncType=calendar
+DEBUG:  delete: cuid=000000004FCBE97B738E984EAF085560B1DD2D50849C2000 suid=20070105T213608Z97fh32byy88@voltaire.local
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:48. Packet logged in /tmp/sync/syncml_server_42.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033004
+DEBUG:  authorized=1 version=1 msgid=4 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,1,0; server(Add,Replace,Delete)=0,0,1
+DEBUG:  Finished at 2007-01-05 22:36:48. Packet logged in /tmp/sync/syncml_server_43.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  New session created: syncmlscpimoutlook1168033013
+DEBUG:  checking auth for user=syncmltest
+DEBUG:  authorized=1 version=1 msgid=1 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  previous sync found for database: calendar; client-ts: 1168033004
+DEBUG:  SyncML: Anchor match, TwoWaySync since 1168033004
+DEBUG:  Create new sync for calendar; synctype=200
+DEBUG:  HandleFinal for state=0
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:55. Packet logged in /tmp/sync/syncml_server_50.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033013
+DEBUG:  authorized=1 version=1 msgid=2 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  HandleFinal for state=1
+DEBUG:  Not yet sending server sync data: special Funambol handling.
+DEBUG:  Received Final from client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:56. Packet logged in /tmp/sync/syncml_server_51.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033013
+DEBUG:  authorized=1 version=1 msgid=3 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  Using device class SyncML_Device_sync4j
+DEBUG:  server create sync output for syncType=calendar
+DEBUG:  Compiling server changes from 2007-01-05 22:36:47 to 2007-01-05 22:36:55
+DEBUG:  sending 0 server changes for syncType=calendar
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+DEBUG:  SyncML: return message completed
+DEBUG:  Finished at 2007-01-05 22:36:56. Packet logged in /tmp/sync/syncml_server_52.xml
+
+DEBUG:  Backend of class SyncML_Backend_Horde created
+DEBUG:  Existing session continued: syncmlscpimoutlook1168033013
+DEBUG:  authorized=1 version=1 msgid=4 source=sc-pim-outlook target=http://voltaire.local/horde/rpc.php user=syncmltest charset=UTF-8 wbxml=
+DEBUG:  HandleFinal for state=3
+DEBUG:  Received Final from client.
+DEBUG:  Sending Final to client.
+INFO:   Successful sync of calendar! Summary: failures=0; client(Add,Replace,Delete,AddReplaces)=0,0,0,0; server(Add,Replace,Delete)=0,0,0
+DEBUG:  Finished at 2007-01-05 22:36:56. Packet logged in /tmp/sync/syncml_server_53.xml
+
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_10.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..b129631
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032875</SessionID><MsgID>1</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>1168032875</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>201</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1168032879</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_11.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..398ab49
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032875</SessionID><MsgID>2</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50049C2000</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50249C2000</SourceRef><Data>201</Data></Status><Get><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_12.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..2600a22
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032875</SessionID><MsgID>3</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Add><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Source><LocURI>20070105223422.1ymx6b99mycg@voltaire.local</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48QnVzeVN0YXR1cz4yPC9CdXN5U3RhdHVzPjxEdXJhdGlvbj42MDwvRHVyYXRpb24+PEVuZD4yMDA3MDEwMlQwNjAwMDBaPC9FbmQ+PElzUmVjdXJyaW5nPjA8L0lzUmVjdXJyaW5nPjxSZW1pbmRlclNldD4wPC9SZW1pbmRlclNldD48U2Vuc2l0aXZpdHk+MDwvU2Vuc2l0aXZpdHk+PFN0YXJ0PjIwMDcwMTAyVDA1MDAwMFo8L1N0YXJ0PjxTdWJqZWN0PnNlcnZlcjIgNi03PC9TdWJqZWN0PjwvYXBwb2ludG1lbnQ+</Data></Item></Add><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Source><LocURI>20070105223408.6wew7kh5jag4@voltaire.local</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48Qm9keT50ZXN0IHV1bWxhdXQ6IMO8PC9Cb2R5PjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PENhdGVnb3JpZXM+aG9saWRheTwvQ2F0ZWdvcmllcz48RHVyYXRpb24+NjA8L0R1cmF0aW9uPjxFbmQ+MjAwNzAxMDZUMDYwMDAwWjwvRW5kPjxJc1JlY3VycmluZz4wPC9Jc1JlY3VycmluZz48TG9jYXRpb24+YWxtb3N0IGV2ZXJ5d2hlcmU8L0xvY2F0aW9uPjxSZW1pbmRlclNldD4wPC9SZW1pbmRlclNldD48U2Vuc2l0aXZpdHk+MDwvU2Vuc2l0aXZpdHk+PFN0YXJ0PjIwMDcwMTA2VDA1MDAwMFo8L1N0YXJ0PjxTdWJqZWN0PmFsbCBkYXkgdGhyZWUga2luZ3Mgc2VydmVyMTwvU3ViamVjdD48L2FwcG9pbnRtZW50Pg==</Data></Item></Add></Sync><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_13.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_13.xml
new file mode 100644 (file)
index 0000000..500bf55
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032875</SessionID><MsgID>4</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>5</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_20.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..1a5fea1
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032964</SessionID><MsgID>1</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>1168032875</Last><Next>1168032964</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1168032879</Last><Next>1168032967</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_21.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..d84cd5f
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032964</SessionID><MsgID>2</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50849C2000</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50449C2000</SourceRef><Data>200</Data></Status><Status><CmdID>5</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50649C2000</SourceRef><Data>200</Data></Status><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_22.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..6223733
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032964</SessionID><MsgID>3</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Add><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Source><LocURI>20070105223456.5x0i61i3nbgo@voltaire.local</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48QnVzeVN0YXR1cz4yPC9CdXN5U3RhdHVzPjxEdXJhdGlvbj42MDwvRHVyYXRpb24+PEVuZD4yMDA3MDEwM1QwNjAwMDBaPC9FbmQ+PElzUmVjdXJyaW5nPjA8L0lzUmVjdXJyaW5nPjxSZW1pbmRlclNldD4wPC9SZW1pbmRlclNldD48U2Vuc2l0aXZpdHk+MDwvU2Vuc2l0aXZpdHk+PFN0YXJ0PjIwMDcwMTAzVDA1MDAwMFo8L1N0YXJ0PjxTdWJqZWN0PnNlcnZlcjk5PC9TdWJqZWN0PjwvYXBwb2ludG1lbnQ+</Data></Item></Add><Replace><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Target><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50049C2000</LocURI></Target><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4xPC9BbGxEYXlFdmVudD48Qm9keT5UZXN0IEF1bWxhdXQ6IMOEPC9Cb2R5PjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PEVuZD4yMDA3LTAxLTAxPC9FbmQ+PElzUmVjdXJyaW5nPjA8L0lzUmVjdXJyaW5nPjxMb2NhdGlvbj5ldmVyeXdoZXJlPC9Mb2NhdGlvbj48UmVtaW5kZXJTZXQ+MDwvUmVtaW5kZXJTZXQ+PFNlbnNpdGl2aXR5PjA8L1NlbnNpdGl2aXR5PjxTdGFydD4yMDA3LTAxLTAxPC9TdGFydD48U3ViamVjdD5uZXcgeWVhciBhbGwgZGF5IGNsaWVudDFzMTwvU3ViamVjdD48L2FwcG9pbnRtZW50Pg==</Data></Item></Replace><Replace><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Target><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50249C2000</LocURI></Target><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxBbGxEYXlFdmVudD4wPC9BbGxEYXlFdmVudD48Qm9keT5BbGFybSAzMG1pbnV0ZXMgYmVmb3JlPC9Cb2R5PjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PER1cmF0aW9uPjMwPC9EdXJhdGlvbj48RW5kPjIwMDcwMTA3VDIyMzAwMFo8L0VuZD48SXNSZWN1cnJpbmc+MDwvSXNSZWN1cnJpbmc+PFJlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjMwPC9SZW1pbmRlck1pbnV0ZXNCZWZvcmVTdGFydD48UmVtaW5kZXJTZXQ+MTwvUmVtaW5kZXJTZXQ+PFNlbnNpdGl2aXR5PjA8L1NlbnNpdGl2aXR5PjxTdGFydD4yMDA3MDEwN1QyMjAwMDBaPC9TdGFydD48U3ViamVjdD5DbGllbnQyczIgMjM6MDAtMjM6MzA8L1N1YmplY3Q+PC9hcHBvaW50bWVudD4=</Data></Item></Replace></Sync><Get><CmdID>7</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_23.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_23.xml
new file mode 100644 (file)
index 0000000..55257d8
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032964</SessionID><MsgID>4</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>6</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_30.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..06191b8
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032979</SessionID><MsgID>1</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>1168032964</Last><Next>1168032979</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1168032967</Last><Next>1168032981</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_31.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..7e6f6ff
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032979</SessionID><MsgID>2</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_32.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..7c32573
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032979</SessionID><MsgID>3</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_33.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_33.xml
new file mode 100644 (file)
index 0000000..1ae0c19
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168032979</SessionID><MsgID>4</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_40.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..f1da184
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033004</SessionID><MsgID>1</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>1168032979</Last><Next>1168033004</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1168032981</Last><Next>1168033007</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_41.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..f973657
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033004</SessionID><MsgID>2</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><SourceRef>000000004FCBE97B738E984EAF085560B1DD2D50A49C2000</SourceRef><Data>200</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_42.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..a68d987
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033004</SessionID><MsgID>3</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Delete><CmdID>4</CmdID><Item><Target><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50849C2000</LocURI></Target></Item></Delete></Sync><Get><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_43.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_43.xml
new file mode 100644 (file)
index 0000000..487a75b
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033004</SessionID><MsgID>4</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_50.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_50.xml
new file mode 100644 (file)
index 0000000..1bb3372
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033013</SessionID><MsgID>1</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>1168033004</Last><Next>1168033013</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1168033007</Last><Next>1168033015</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_51.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_51.xml
new file mode 100644 (file)
index 0000000..b527cca
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033013</SessionID><MsgID>2</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_52.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_52.xml
new file mode 100644 (file)
index 0000000..b9bb6dc
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033013</SessionID><MsgID>3</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Sync><CmdID>3</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_53.xml b/framework/SyncML/tests/testcase_funambol_outlook3015_calendar/syncml_server_53.xml
new file mode 100644 (file)
index 0000000..ff0bec5
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>1168033013</SessionID><MsgID>4</MsgID><Target><LocURI>sc-pim-outlook</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>sc-pim-outlook</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..faa0893
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32288706</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>0</Last>
+<Next>32288706</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..d7aa4c9
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32288706</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1161009197</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Replace><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>184558443</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48YXBwb2ludG1lbnQ+PFN1YmplY3Q+Y2xpZW50MTwvU3ViamVjdD48TG9jYXRpb24+TG9jYXRpb248L0xvY2F0aW9uPjxDYXRlZ29yaWVzPkdlc2Now6RmdGxpY2g8L0NhdGVnb3JpZXM+PFJlbWluZGVyU291bmRGaWxlPnJlbWluZGVyPC9SZW1pbmRlclNvdW5kRmlsZT48Qm9keT7DnGJlcnNpY2h0PC9Cb2R5PjxTdGFydD4yMDA2MTAxOFQwMzMwMDBaPC9TdGFydD48RW5kPjIwMDYxMDE4VDA2MDAwMFo8L0VuZD48QWxsRGF5RXZlbnQ+MDwvQWxsRGF5RXZlbnQ+PFJlbWluZGVyU2V0PjE8L1JlbWluZGVyU2V0PjxCdXN5U3RhdHVzPjA8L0J1c3lTdGF0dXM+PFNlbnNpdGl2aXR5PjA8L1NlbnNpdGl2aXR5PjxSZW1pbmRlck9wdGlvbnM+MTM8L1JlbWluZGVyT3B0aW9ucz48UmVtaW5kZXJNaW51dGVzQmVmb3JlU3RhcnQ+MTU8L1JlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjxJc1JlY3VycmluZz4wPC9Jc1JlY3VycmluZz48UmVjdXJyZW5jZVR5cGU+MDwvUmVjdXJyZW5jZVR5cGU+PEludGVydmFsPjA8L0ludGVydmFsPjxEYXlPZk1vbnRoPjA8L0RheU9mTW9udGg+PE1vbnRoT2ZZZWFyPjA8L01vbnRoT2ZZZWFyPjxEYXlPZldlZWtNYXNrPjA8L0RheU9mV2Vla01hc2s+PEluc3RhbmNlPjA8L0luc3RhbmNlPjxQYXR0ZXJuU3RhcnREYXRlPjIwMDYxMDE3VDIyMDAwMFo8L1BhdHRlcm5TdGFydERhdGU+PE5vRW5kRGF0ZT4xPC9Ob0VuZERhdGU+PFBhdHRlcm5FbmREYXRlPjwvUGF0dGVybkVuZERhdGU+PE9jY3VycmVuY2VzPjA8L09jY3VycmVuY2VzPjwvYXBwb2ludG1lbnQ+</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..5210bf4
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32288706</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_13.xml
new file mode 100644 (file)
index 0000000..71b7fff
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32288706</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061016163151.x61x2ng9dcg@trinity.wg.de</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>4</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061016163151.x61x2ng9dcg@trinity.wg.de</LocURI>
+</Target>
+<Source><LocURI>83899779</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..64804b6
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291312</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>32288706</Last>
+<Next>32291312</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..a224a99
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291312</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1161009979</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Add><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>67117561</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48YXBwb2ludG1lbnQ+PFN1YmplY3Q+Y2xpZW50MjwvU3ViamVjdD48TG9jYXRpb24+PC9Mb2NhdGlvbj48Q2F0ZWdvcmllcz5HZXNjaMOkZnRsaWNoPC9DYXRlZ29yaWVzPjxSZW1pbmRlclNvdW5kRmlsZT5yZW1pbmRlcjwvUmVtaW5kZXJTb3VuZEZpbGU+PEJvZHk+PC9Cb2R5PjxTdGFydD4yMDA2MTAyOFQwNTAwMDBaPC9TdGFydD48RW5kPjIwMDYxMDI4VDA5MzAwMFo8L0VuZD48QWxsRGF5RXZlbnQ+MDwvQWxsRGF5RXZlbnQ+PFJlbWluZGVyU2V0PjA8L1JlbWluZGVyU2V0PjxCdXN5U3RhdHVzPjM8L0J1c3lTdGF0dXM+PFNlbnNpdGl2aXR5PjI8L1NlbnNpdGl2aXR5PjxSZW1pbmRlck9wdGlvbnM+MTM8L1JlbWluZGVyT3B0aW9ucz48UmVtaW5kZXJNaW51dGVzQmVmb3JlU3RhcnQ+MTU8L1JlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjxJc1JlY3VycmluZz4wPC9Jc1JlY3VycmluZz48UmVjdXJyZW5jZVR5cGU+MDwvUmVjdXJyZW5jZVR5cGU+PEludGVydmFsPjA8L0ludGVydmFsPjxEYXlPZk1vbnRoPjA8L0RheU9mTW9udGg+PE1vbnRoT2ZZZWFyPjA8L01vbnRoT2ZZZWFyPjxEYXlPZldlZWtNYXNrPjA8L0RheU9mV2Vla01hc2s+PEluc3RhbmNlPjA8L0luc3RhbmNlPjxQYXR0ZXJuU3RhcnREYXRlPjIwMDYxMDI3VDIyMDAwMFo8L1BhdHRlcm5TdGFydERhdGU+PE5vRW5kRGF0ZT4xPC9Ob0VuZERhdGU+PFBhdHRlcm5FbmREYXRlPjwvUGF0dGVybkVuZERhdGU+PE9jY3VycmVuY2VzPjA8L09jY3VycmVuY2VzPjwvYXBwb2ludG1lbnQ+</Data>
+</Item>
+</Add>
+<Replace><CmdID>5</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>83899779</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48YXBwb2ludG1lbnQ+PFN1YmplY3Q+c2VydmVyMWMxPC9TdWJqZWN0PjxMb2NhdGlvbj5Mb2NhdGlvbiBTZXJ2ZXI8L0xvY2F0aW9uPjxDYXRlZ29yaWVzPk15IENhdGVnb3J5PC9DYXRlZ29yaWVzPjxSZW1pbmRlclNvdW5kRmlsZT5yZW1pbmRlcjwvUmVtaW5kZXJTb3VuZEZpbGU+PEJvZHk+w5xiZXJzaWNodDwvQm9keT48U3RhcnQ+MjAwNjEwMjNUMDQwMDAwWjwvU3RhcnQ+PEVuZD4yMDA2MTAyM1QwOTAwMDBaPC9FbmQ+PEFsbERheUV2ZW50PjA8L0FsbERheUV2ZW50PjxSZW1pbmRlclNldD4wPC9SZW1pbmRlclNldD48QnVzeVN0YXR1cz4yPC9CdXN5U3RhdHVzPjxTZW5zaXRpdml0eT4wPC9TZW5zaXRpdml0eT48UmVtaW5kZXJPcHRpb25zPjEzPC9SZW1pbmRlck9wdGlvbnM+PFJlbWluZGVyTWludXRlc0JlZm9yZVN0YXJ0PjE1PC9SZW1pbmRlck1pbnV0ZXNCZWZvcmVTdGFydD48SXNSZWN1cnJpbmc+MDwvSXNSZWN1cnJpbmc+PFJlY3VycmVuY2VUeXBlPjA8L1JlY3VycmVuY2VUeXBlPjxJbnRlcnZhbD4wPC9JbnRlcnZhbD48RGF5T2ZNb250aD4wPC9EYXlPZk1vbnRoPjxNb250aE9mWWVhcj4wPC9Nb250aE9mWWVhcj48RGF5T2ZXZWVrTWFzaz4wPC9EYXlPZldlZWtNYXNrPjxJbnN0YW5jZT4wPC9JbnN0YW5jZT48UGF0dGVyblN0YXJ0RGF0ZT4yMDA2MTAyMlQyMjAwMDBaPC9QYXR0ZXJuU3RhcnREYXRlPjxOb0VuZERhdGU+MTwvTm9FbmREYXRlPjxQYXR0ZXJuRW5kRGF0ZT48L1BhdHRlcm5FbmREYXRlPjxPY2N1cnJlbmNlcz4wPC9PY2N1cnJlbmNlcz48L2FwcG9pbnRtZW50Pg==</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..8f8e032
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291312</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_23.xml
new file mode 100644 (file)
index 0000000..00b213a
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291312</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061016163645.7g6pl2sp85wc@trinity.wg.de</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>4</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>6</CmdRef>
+<Cmd>Replace</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>184558443</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>5</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061016163645.7g6pl2sp85wc@trinity.wg.de</LocURI>
+</Target>
+<Source><LocURI>201340279</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..8d518c3
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291424</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>32291312</Last>
+<Next>32291424</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..7241dc8
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291424</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1161009995</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..ec517b8
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291424</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_33.xml
new file mode 100644 (file)
index 0000000..bb8629d
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32291424</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..34e5d00
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32302258</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Meta><Anchor><Last>32291424</Last>
+<Next>32302258</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..6c54296
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32302258</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1161010852</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+<Delete><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sife</Type>
+</Meta>
+<Item><Source><LocURI>201340279</LocURI>
+</Source>
+</Item>
+</Delete>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..5b2968b
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32302258</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>calendar</LocURI>
+</Target>
+<Source><LocURI>calendar</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_client_43.xml
new file mode 100644 (file)
index 0000000..264b932
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>32302258</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>calendar</TargetRef>
+<SourceRef>calendar</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Delete</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>67117561</LocURI>
+</Source>
+</Item>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..182d203
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32288706</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>508</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>32288706</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>201</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1161009197</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..04fca1c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32288706</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><SourceRef>184558443</SourceRef><Data>201</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..9978f2d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32288706</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Source><LocURI>20061016163151.x61x2ng9dcg@trinity.wg.de</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxEdXJhdGlvbj4wPC9EdXJhdGlvbj48QWxsRGF5RXZlbnQ+RmFsc2U8L0FsbERheUV2ZW50PjxTdGFydD4yMDA2MTAyM1QwNDAwMDBaPC9TdGFydD48RW5kPjIwMDYxMDIzVDA5MDAwMFo8L0VuZD48U3ViamVjdD5zZXJ2ZXIxPC9TdWJqZWN0PjxCb2R5PsOcYmVyc2ljaHQ8L0JvZHk+PENhdGVnb3JpZXM+TXkgQ2F0ZWdvcnk8L0NhdGVnb3JpZXM+PExvY2F0aW9uPkxvY2F0aW9uIFNlcnZlcjwvTG9jYXRpb24+PElzUmVjdXJyaW5nPkZhbHNlPC9Jc1JlY3VycmluZz48TWVldGluZ1N0YXR1cz4wPC9NZWV0aW5nU3RhdHVzPjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PC9hcHBvaW50bWVudD4=</Data></Item></Add></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_13.xml
new file mode 100644 (file)
index 0000000..478bd46
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32288706</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>4</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..ca5b40d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291312</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>32288706</Last><Next>32291312</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1161009197</Last><Next>1161009979</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..bc9f44a
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291312</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>67117561</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><SourceRef>83899779</SourceRef><Data>200</Data></Status><Get><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..8e64f8c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291312</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Source><LocURI>20061016163645.7g6pl2sp85wc@trinity.wg.de</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxEdXJhdGlvbj4wPC9EdXJhdGlvbj48QWxsRGF5RXZlbnQ+RmFsc2U8L0FsbERheUV2ZW50PjxTdGFydD4yMDA2MTAyNVQwNDAwMDBaPC9TdGFydD48RW5kPjIwMDYxMDI1VDA1MDAwMFo8L0VuZD48U3ViamVjdD5zZXJ2ZXIyPC9TdWJqZWN0PjxDYXRlZ29yaWVzPkdlc2Now6RmdGxpY2g8L0NhdGVnb3JpZXM+PElzUmVjdXJyaW5nPkZhbHNlPC9Jc1JlY3VycmluZz48TWVldGluZ1N0YXR1cz4wPC9NZWV0aW5nU3RhdHVzPjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PC9hcHBvaW50bWVudD4=</Data></Item></Add><Replace><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sife</Type></Meta><Item><Target><LocURI>184558443</LocURI></Target><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGFwcG9pbnRtZW50PjxEdXJhdGlvbj4wPC9EdXJhdGlvbj48QWxsRGF5RXZlbnQ+RmFsc2U8L0FsbERheUV2ZW50PjxTdGFydD4yMDA2MTAxOFQwMzMwMDBaPC9TdGFydD48RW5kPjIwMDYxMDE4VDA2MDAwMFo8L0VuZD48U3ViamVjdD5jbGllbnQxczE8L1N1YmplY3Q+PEJvZHk+w5xiZXJzaWNodDwvQm9keT48Q2F0ZWdvcmllcz5HZXNjaMOkZnRsaWNoPC9DYXRlZ29yaWVzPjxMb2NhdGlvbj5Mb2NhdGlvbjwvTG9jYXRpb24+PElzUmVjdXJyaW5nPkZhbHNlPC9Jc1JlY3VycmluZz48TWVldGluZ1N0YXR1cz4wPC9NZWV0aW5nU3RhdHVzPjxCdXN5U3RhdHVzPjI8L0J1c3lTdGF0dXM+PC9hcHBvaW50bWVudD4=</Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_23.xml
new file mode 100644 (file)
index 0000000..4ab3860
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291312</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>5</CmdRef><Cmd>Map</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..33f24e6
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291424</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>32291312</Last><Next>32291424</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1161009979</Last><Next>1161009995</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..40b249d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291424</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..7b40c4d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291424</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_33.xml
new file mode 100644 (file)
index 0000000..a9a326a
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32291424</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..1850fcd
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32302258</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>32291424</Last><Next>32302258</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1161009995</Last><Next>1161010852</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..f320aee
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32302258</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>calendar</TargetRef><SourceRef>calendar</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><SourceRef>201340279</SourceRef><Data>200</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..6ed6779
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32302258</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>calendar</LocURI></Target><Source><LocURI>calendar</LocURI></Source><Delete><CmdID>5</CmdID><Item><Target><LocURI>67117561</LocURI></Target></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_calendar/syncml_server_43.xml
new file mode 100644 (file)
index 0000000..a49852c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>32302258</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..ccb161e
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26429128</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Meta><Anchor><Last>0</Last>
+<Next>26429128</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..3f10ee0
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26429128</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160775338</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Replace><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifc</Type>
+</Meta>
+<Item><Source><LocURI>83899317</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y29udGFjdD48Rmlyc3ROYW1lPjwvRmlyc3ROYW1lPjxMYXN0TmFtZT5DbGllbnQxPC9MYXN0TmFtZT48Q29tcGFueU5hbWU+Q29tcGFueTwvQ29tcGFueU5hbWU+PEJ1c2luZXNzRmF4TnVtYmVyPjwvQnVzaW5lc3NGYXhOdW1iZXI+PERlcGFydG1lbnQ+PC9EZXBhcnRtZW50PjxFbWFpbDFBZGRyZXNzPmNsaWVudEBleGFtcGxlLmNvbTwvRW1haWwxQWRkcmVzcz48TW9iaWxlVGVsZXBob25lTnVtYmVyPjwvTW9iaWxlVGVsZXBob25lTnVtYmVyPjxPZmZpY2VMb2NhdGlvbj48L09mZmljZUxvY2F0aW9uPjxQYWdlck51bWJlcj48L1BhZ2VyTnVtYmVyPjxCdXNpbmVzc1RlbGVwaG9uZU51bWJlcj41NTUtMTIzPC9CdXNpbmVzc1RlbGVwaG9uZU51bWJlcj48Sm9iVGl0bGU+PC9Kb2JUaXRsZT48SG9tZVRlbGVwaG9uZU51bWJlcj48L0hvbWVUZWxlcGhvbmVOdW1iZXI+PEVtYWlsMkFkZHJlc3M+PC9FbWFpbDJBZGRyZXNzPjxTcG91c2U+PC9TcG91c2U+PEVtYWlsM0FkZHJlc3M+PC9FbWFpbDNBZGRyZXNzPjxIb21lMlRlbGVwaG9uZU51bWJlcj48L0hvbWUyVGVsZXBob25lTnVtYmVyPjxIb21lRmF4TnVtYmVyPjwvSG9tZUZheE51bWJlcj48Q2FyVGVsZXBob25lTnVtYmVyPjwvQ2FyVGVsZXBob25lTnVtYmVyPjxBc3Npc3RhbnROYW1lPjwvQXNzaXN0YW50TmFtZT48QXNzaXN0YW50VGVsZXBob25lTnVtYmVyPjwvQXNzaXN0YW50VGVsZXBob25lTnVtYmVyPjxDaGlsZHJlbj48L0NoaWxkcmVuPjxDYXRlZ29yaWVzPjwvQ2F0ZWdvcmllcz48V2ViUGFnZT48L1dlYlBhZ2U+PEJ1c2luZXNzMlRlbGVwaG9uZU51bWJlcj48L0J1c2luZXNzMlRlbGVwaG9uZU51bWJlcj48UmFkaW9UZWxlcGhvbmVOdW1iZXI+PC9SYWRpb1RlbGVwaG9uZU51bWJlcj48RmlsZUFzPkNsaWVudDE8L0ZpbGVBcz48WW9taUNvbXBhbnlOYW1lPjwvWW9taUNvbXBhbnlOYW1lPjxZb21pRmlyc3ROYW1lPjwvWW9taUZpcnN0TmFtZT48WW9taUxhc3ROYW1lPjwvWW9taUxhc3ROYW1lPjxUaXRsZT48L1RpdGxlPjxNaWRkbGVOYW1lPjwvTWlkZGxlTmFtZT48U3VmZml4PjwvU3VmZml4PjxIb21lQWRkcmVzc1N0cmVldD48L0hvbWVBZGRyZXNzU3RyZWV0PjxIb21lQWRkcmVzc0NpdHk+PC9Ib21lQWRkcmVzc0NpdHk+PEhvbWVBZGRyZXNzU3RhdGU+PC9Ib21lQWRkcmVzc1N0YXRlPjxIb21lQWRkcmVzc1Bvc3RhbENvZGU+PC9Ib21lQWRkcmVzc1Bvc3RhbENvZGU+PEhvbWVBZGRyZXNzQ291bnRyeT48L0hvbWVBZGRyZXNzQ291bnRyeT48T3RoZXJBZGRyZXNzU3RyZWV0PjwvT3RoZXJBZGRyZXNzU3RyZWV0PjxPdGhlckFkZHJlc3NDaXR5PjwvT3RoZXJBZGRyZXNzQ2l0eT48T3RoZXJBZGRyZXNzUG9zdGFsQ29kZT48L090aGVyQWRkcmVzc1Bvc3RhbENvZGU+PE90aGVyQWRkcmVzc0NvdW50cnk+PC9PdGhlckFkZHJlc3NDb3VudHJ5PjxPdGhlckFkZHJlc3NTdGF0ZT48L090aGVyQWRkcmVzc1N0YXRlPjxCdXNpbmVzc0FkZHJlc3NTdHJlZXQ+PC9CdXNpbmVzc0FkZHJlc3NTdHJlZXQ+PEJ1c2luZXNzQWRkcmVzc0NpdHk+PC9CdXNpbmVzc0FkZHJlc3NDaXR5PjxCdXNpbmVzc0FkZHJlc3NTdGF0ZT48L0J1c2luZXNzQWRkcmVzc1N0YXRlPjxCdXNpbmVzc0FkZHJlc3NQb3N0YWxDb2RlPjwvQnVzaW5lc3NBZGRyZXNzUG9zdGFsQ29kZT48QnVzaW5lc3NBZGRyZXNzQ291bnRyeT48L0J1c2luZXNzQWRkcmVzc0NvdW50cnk+PEJvZHk+PC9Cb2R5PjxCaXJ0aGRheT48L0JpcnRoZGF5PjxBbm5pdmVyc2FyeT48L0Fubml2ZXJzYXJ5PjwvY29udGFjdD4=</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..609753d
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26429128</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_13.xml
new file mode 100644 (file)
index 0000000..9e5ab57
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26429128</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061013233527.htyrg8wmc2o@192.168.60.3</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>4</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061013233527.htyrg8wmc2o@192.168.60.3</LocURI>
+</Target>
+<Source><LocURI>50344628</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..0d93e2a
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430116</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Meta><Anchor><Last>26429128</Last>
+<Next>26430116</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..ab1e7d9
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430116</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160775641</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Add><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifc</Type>
+</Meta>
+<Item><Source><LocURI>251660424</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y29udGFjdD48Rmlyc3ROYW1lPjwvRmlyc3ROYW1lPjxMYXN0TmFtZT5DbGllbnQyPC9MYXN0TmFtZT48Q29tcGFueU5hbWU+PC9Db21wYW55TmFtZT48QnVzaW5lc3NGYXhOdW1iZXI+PC9CdXNpbmVzc0ZheE51bWJlcj48RGVwYXJ0bWVudD48L0RlcGFydG1lbnQ+PEVtYWlsMUFkZHJlc3M+Y2xpZW50MkBleGFtcGxlLmNvbTwvRW1haWwxQWRkcmVzcz48TW9iaWxlVGVsZXBob25lTnVtYmVyPjwvTW9iaWxlVGVsZXBob25lTnVtYmVyPjxPZmZpY2VMb2NhdGlvbj48L09mZmljZUxvY2F0aW9uPjxQYWdlck51bWJlcj48L1BhZ2VyTnVtYmVyPjxCdXNpbmVzc1RlbGVwaG9uZU51bWJlcj4rNDkgNTIxIDE3MDM3MjwvQnVzaW5lc3NUZWxlcGhvbmVOdW1iZXI+PEpvYlRpdGxlPjwvSm9iVGl0bGU+PEhvbWVUZWxlcGhvbmVOdW1iZXI+PC9Ib21lVGVsZXBob25lTnVtYmVyPjxFbWFpbDJBZGRyZXNzPjwvRW1haWwyQWRkcmVzcz48U3BvdXNlPjwvU3BvdXNlPjxFbWFpbDNBZGRyZXNzPjwvRW1haWwzQWRkcmVzcz48SG9tZTJUZWxlcGhvbmVOdW1iZXI+PC9Ib21lMlRlbGVwaG9uZU51bWJlcj48SG9tZUZheE51bWJlcj48L0hvbWVGYXhOdW1iZXI+PENhclRlbGVwaG9uZU51bWJlcj48L0NhclRlbGVwaG9uZU51bWJlcj48QXNzaXN0YW50TmFtZT48L0Fzc2lzdGFudE5hbWU+PEFzc2lzdGFudFRlbGVwaG9uZU51bWJlcj48L0Fzc2lzdGFudFRlbGVwaG9uZU51bWJlcj48Q2hpbGRyZW4+PC9DaGlsZHJlbj48Q2F0ZWdvcmllcz48L0NhdGVnb3JpZXM+PFdlYlBhZ2U+PC9XZWJQYWdlPjxCdXNpbmVzczJUZWxlcGhvbmVOdW1iZXI+PC9CdXNpbmVzczJUZWxlcGhvbmVOdW1iZXI+PFJhZGlvVGVsZXBob25lTnVtYmVyPjwvUmFkaW9UZWxlcGhvbmVOdW1iZXI+PEZpbGVBcz5DbGllbnQyPC9GaWxlQXM+PFlvbWlDb21wYW55TmFtZT48L1lvbWlDb21wYW55TmFtZT48WW9taUZpcnN0TmFtZT48L1lvbWlGaXJzdE5hbWU+PFlvbWlMYXN0TmFtZT48L1lvbWlMYXN0TmFtZT48VGl0bGU+PC9UaXRsZT48TWlkZGxlTmFtZT48L01pZGRsZU5hbWU+PFN1ZmZpeD48L1N1ZmZpeD48SG9tZUFkZHJlc3NTdHJlZXQ+PC9Ib21lQWRkcmVzc1N0cmVldD48SG9tZUFkZHJlc3NDaXR5PjwvSG9tZUFkZHJlc3NDaXR5PjxIb21lQWRkcmVzc1N0YXRlPjwvSG9tZUFkZHJlc3NTdGF0ZT48SG9tZUFkZHJlc3NQb3N0YWxDb2RlPjwvSG9tZUFkZHJlc3NQb3N0YWxDb2RlPjxIb21lQWRkcmVzc0NvdW50cnk+PC9Ib21lQWRkcmVzc0NvdW50cnk+PE90aGVyQWRkcmVzc1N0cmVldD48L090aGVyQWRkcmVzc1N0cmVldD48T3RoZXJBZGRyZXNzQ2l0eT48L090aGVyQWRkcmVzc0NpdHk+PE90aGVyQWRkcmVzc1Bvc3RhbENvZGU+PC9PdGhlckFkZHJlc3NQb3N0YWxDb2RlPjxPdGhlckFkZHJlc3NDb3VudHJ5PjwvT3RoZXJBZGRyZXNzQ291bnRyeT48T3RoZXJBZGRyZXNzU3RhdGU+PC9PdGhlckFkZHJlc3NTdGF0ZT48QnVzaW5lc3NBZGRyZXNzU3RyZWV0PjwvQnVzaW5lc3NBZGRyZXNzU3RyZWV0PjxCdXNpbmVzc0FkZHJlc3NDaXR5PjwvQnVzaW5lc3NBZGRyZXNzQ2l0eT48QnVzaW5lc3NBZGRyZXNzU3RhdGU+PC9CdXNpbmVzc0FkZHJlc3NTdGF0ZT48QnVzaW5lc3NBZGRyZXNzUG9zdGFsQ29kZT48L0J1c2luZXNzQWRkcmVzc1Bvc3RhbENvZGU+PEJ1c2luZXNzQWRkcmVzc0NvdW50cnk+PC9CdXNpbmVzc0FkZHJlc3NDb3VudHJ5PjxCb2R5PjwvQm9keT48QmlydGhkYXk+PC9CaXJ0aGRheT48QW5uaXZlcnNhcnk+PC9Bbm5pdmVyc2FyeT48L2NvbnRhY3Q+</Data>
+</Item>
+</Add>
+<Replace><CmdID>5</CmdID>
+<Meta><Type>text/x-s4j-sifc</Type>
+</Meta>
+<Item><Source><LocURI>50344628</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y29udGFjdD48Rmlyc3ROYW1lPlNlcnZlcjFjMTwvRmlyc3ROYW1lPjxMYXN0TmFtZT48L0xhc3ROYW1lPjxDb21wYW55TmFtZT48L0NvbXBhbnlOYW1lPjxCdXNpbmVzc0ZheE51bWJlcj48L0J1c2luZXNzRmF4TnVtYmVyPjxEZXBhcnRtZW50PjwvRGVwYXJ0bWVudD48RW1haWwxQWRkcmVzcz5zZXJ2ZXIxQGV4YW1wbGUuY29tPC9FbWFpbDFBZGRyZXNzPjxNb2JpbGVUZWxlcGhvbmVOdW1iZXI+PC9Nb2JpbGVUZWxlcGhvbmVOdW1iZXI+PE9mZmljZUxvY2F0aW9uPjwvT2ZmaWNlTG9jYXRpb24+PFBhZ2VyTnVtYmVyPjwvUGFnZXJOdW1iZXI+PEJ1c2luZXNzVGVsZXBob25lTnVtYmVyPjwvQnVzaW5lc3NUZWxlcGhvbmVOdW1iZXI+PEpvYlRpdGxlPjwvSm9iVGl0bGU+PEhvbWVUZWxlcGhvbmVOdW1iZXI+PC9Ib21lVGVsZXBob25lTnVtYmVyPjxFbWFpbDJBZGRyZXNzPjwvRW1haWwyQWRkcmVzcz48U3BvdXNlPjwvU3BvdXNlPjxFbWFpbDNBZGRyZXNzPjwvRW1haWwzQWRkcmVzcz48SG9tZTJUZWxlcGhvbmVOdW1iZXI+PC9Ib21lMlRlbGVwaG9uZU51bWJlcj48SG9tZUZheE51bWJlcj48L0hvbWVGYXhOdW1iZXI+PENhclRlbGVwaG9uZU51bWJlcj48L0NhclRlbGVwaG9uZU51bWJlcj48QXNzaXN0YW50TmFtZT48L0Fzc2lzdGFudE5hbWU+PEFzc2lzdGFudFRlbGVwaG9uZU51bWJlcj48L0Fzc2lzdGFudFRlbGVwaG9uZU51bWJlcj48Q2hpbGRyZW4+PC9DaGlsZHJlbj48Q2F0ZWdvcmllcz48L0NhdGVnb3JpZXM+PFdlYlBhZ2U+PC9XZWJQYWdlPjxCdXNpbmVzczJUZWxlcGhvbmVOdW1iZXI+PC9CdXNpbmVzczJUZWxlcGhvbmVOdW1iZXI+PFJhZGlvVGVsZXBob25lTnVtYmVyPjwvUmFkaW9UZWxlcGhvbmVOdW1iZXI+PEZpbGVBcz5TZXJ2ZXIxYzE8L0ZpbGVBcz48WW9taUNvbXBhbnlOYW1lPjwvWW9taUNvbXBhbnlOYW1lPjxZb21pRmlyc3ROYW1lPjwvWW9taUZpcnN0TmFtZT48WW9taUxhc3ROYW1lPjwvWW9taUxhc3ROYW1lPjxUaXRsZT48L1RpdGxlPjxNaWRkbGVOYW1lPjwvTWlkZGxlTmFtZT48U3VmZml4PjwvU3VmZml4PjxIb21lQWRkcmVzc1N0cmVldD7DnGJlcnNpY2h0PC9Ib21lQWRkcmVzc1N0cmVldD48SG9tZUFkZHJlc3NDaXR5PjwvSG9tZUFkZHJlc3NDaXR5PjxIb21lQWRkcmVzc1N0YXRlPjwvSG9tZUFkZHJlc3NTdGF0ZT48SG9tZUFkZHJlc3NQb3N0YWxDb2RlPjwvSG9tZUFkZHJlc3NQb3N0YWxDb2RlPjxIb21lQWRkcmVzc0NvdW50cnk+PC9Ib21lQWRkcmVzc0NvdW50cnk+PE90aGVyQWRkcmVzc1N0cmVldD48L090aGVyQWRkcmVzc1N0cmVldD48T3RoZXJBZGRyZXNzQ2l0eT48L090aGVyQWRkcmVzc0NpdHk+PE90aGVyQWRkcmVzc1Bvc3RhbENvZGU+PC9PdGhlckFkZHJlc3NQb3N0YWxDb2RlPjxPdGhlckFkZHJlc3NDb3VudHJ5PjwvT3RoZXJBZGRyZXNzQ291bnRyeT48T3RoZXJBZGRyZXNzU3RhdGU+PC9PdGhlckFkZHJlc3NTdGF0ZT48QnVzaW5lc3NBZGRyZXNzU3RyZWV0PjwvQnVzaW5lc3NBZGRyZXNzU3RyZWV0PjxCdXNpbmVzc0FkZHJlc3NDaXR5PjwvQnVzaW5lc3NBZGRyZXNzQ2l0eT48QnVzaW5lc3NBZGRyZXNzU3RhdGU+PC9CdXNpbmVzc0FkZHJlc3NTdGF0ZT48QnVzaW5lc3NBZGRyZXNzUG9zdGFsQ29kZT48L0J1c2luZXNzQWRkcmVzc1Bvc3RhbENvZGU+PEJ1c2luZXNzQWRkcmVzc0NvdW50cnk+PC9CdXNpbmVzc0FkZHJlc3NDb3VudHJ5PjxCb2R5PjwvQm9keT48QmlydGhkYXk+PC9CaXJ0aGRheT48QW5uaXZlcnNhcnk+PC9Bbm5pdmVyc2FyeT48L2NvbnRhY3Q+</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..1336c00
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430116</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_23.xml
new file mode 100644 (file)
index 0000000..c4f82e9
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430116</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061013233958.53s4q2q5bwso@192.168.60.3</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>4</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>6</CmdRef>
+<Cmd>Replace</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>83899317</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>5</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061013233958.53s4q2q5bwso@192.168.60.3</LocURI>
+</Target>
+<Source><LocURI>151008046</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..9f566b1
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430524</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Meta><Anchor><Last>26430116</Last>
+<Next>26430524</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..0654504
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430524</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160775756</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..d3523e1
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430524</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_33.xml
new file mode 100644 (file)
index 0000000..53430bb
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430524</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..adad19b
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430854</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Meta><Anchor><Last>26430524</Last>
+<Next>26430854</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..043249a
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430854</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160775842</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+<Delete><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifc</Type>
+</Meta>
+<Item><Source><LocURI>83899317</LocURI>
+</Source>
+</Item>
+<Item><Source><LocURI>151008046</LocURI>
+</Source>
+</Item>
+</Delete>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..37b6323
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430854</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>contacts</LocURI>
+</Target>
+<Source><LocURI>contact</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_client_43.xml
new file mode 100644 (file)
index 0000000..480b85c
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>26430854</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>contacts</TargetRef>
+<SourceRef>contact</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Delete</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>251660424</LocURI>
+</Source>
+</Item>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..49f3d6e
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26429128</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>508</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>26429128</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>201</Data><Item><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1160775338</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..0ebbd6b
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26429128</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><SourceRef>83899317</SourceRef><Data>201</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..187413c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26429128</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifc</Type></Meta><Item><Source><LocURI>20061013233527.htyrg8wmc2o@192.168.60.3</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGNvbnRhY3Q+PEZpbGVBcz5zZXJ2ZXIxPC9GaWxlQXM+PEVtYWlsMUFkZHJlc3M+c2VydmVyMUBleGFtcGxlLmNvbTwvRW1haWwxQWRkcmVzcz48RW1haWwxQWRkcmVzc1R5cGU+U01UUDwvRW1haWwxQWRkcmVzc1R5cGU+PE5pY2tOYW1lPjwvTmlja05hbWU+PEhvbWVUZWxlcGhvbmVOdW1iZXI+PC9Ib21lVGVsZXBob25lTnVtYmVyPjxCdXNpbmVzc1RlbGVwaG9uZU51bWJlcj48L0J1c2luZXNzVGVsZXBob25lTnVtYmVyPjxNb2JpbGVUZWxlcGhvbmVOdW1iZXI+PC9Nb2JpbGVUZWxlcGhvbmVOdW1iZXI+PEJ1c2luZXNzRmF4TnVtYmVyPjwvQnVzaW5lc3NGYXhOdW1iZXI+PEpvYlRpdGxlPjwvSm9iVGl0bGU+PENvbXBhbnlOYW1lPjwvQ29tcGFueU5hbWU+PEJvZHk+PC9Cb2R5PjxMYXN0TmFtZT5zZXJ2ZXIxPC9MYXN0TmFtZT48Rmlyc3ROYW1lPjwvRmlyc3ROYW1lPjxNaWRkbGVOYW1lPjwvTWlkZGxlTmFtZT48VGl0bGU+PC9UaXRsZT48U3VmZml4PjwvU3VmZml4PjxIb21lQWRkcmVzc1N0cmVldD7DnGJlcnNpY2h0PC9Ib21lQWRkcmVzc1N0cmVldD48SG9tZUFkZHJlc3NDaXR5PjwvSG9tZUFkZHJlc3NDaXR5PjxIb21lQWRkcmVzc1N0YXRlPjwvSG9tZUFkZHJlc3NTdGF0ZT48SG9tZUFkZHJlc3NQb3N0YWxDb2RlPjwvSG9tZUFkZHJlc3NQb3N0YWxDb2RlPjxIb21lQWRkcmVzc0NvdW50cnk+PC9Ib21lQWRkcmVzc0NvdW50cnk+PEhvbWVBZGRyZXNzUG9zdE9mZmljZUJveD48L0hvbWVBZGRyZXNzUG9zdE9mZmljZUJveD48V29ya0FkZHJlc3NTdHJlZXQ+PC9Xb3JrQWRkcmVzc1N0cmVldD48V29ya0FkZHJlc3NDaXR5PjwvV29ya0FkZHJlc3NDaXR5PjxXb3JrQWRkcmVzc1N0YXRlPjwvV29ya0FkZHJlc3NTdGF0ZT48V29ya0FkZHJlc3NQb3N0YWxDb2RlPjwvV29ya0FkZHJlc3NQb3N0YWxDb2RlPjxXb3JrQWRkcmVzc0NvdW50cnk+PC9Xb3JrQWRkcmVzc0NvdW50cnk+PFdvcmtBZGRyZXNzUG9zdE9mZmljZUJveD48L1dvcmtBZGRyZXNzUG9zdE9mZmljZUJveD48L2NvbnRhY3Q+</Data></Item></Add></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_13.xml
new file mode 100644 (file)
index 0000000..8b34930
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26429128</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>4</CmdRef><Cmd>Map</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..9388d73
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430116</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>26429128</Last><Next>26430116</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160775338</Last><Next>1160775641</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..24af887
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430116</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>251660424</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><SourceRef>50344628</SourceRef><Data>200</Data></Status><Get><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..8632b89
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430116</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifc</Type></Meta><Item><Source><LocURI>20061013233958.53s4q2q5bwso@192.168.60.3</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGNvbnRhY3Q+PEZpbGVBcz5zZXJ2ZXIyPC9GaWxlQXM+PEVtYWlsMUFkZHJlc3M+PC9FbWFpbDFBZGRyZXNzPjxFbWFpbDFBZGRyZXNzVHlwZT5TTVRQPC9FbWFpbDFBZGRyZXNzVHlwZT48Tmlja05hbWU+PC9OaWNrTmFtZT48SG9tZVRlbGVwaG9uZU51bWJlcj48L0hvbWVUZWxlcGhvbmVOdW1iZXI+PEJ1c2luZXNzVGVsZXBob25lTnVtYmVyPjEyMzQtNTY3ODwvQnVzaW5lc3NUZWxlcGhvbmVOdW1iZXI+PE1vYmlsZVRlbGVwaG9uZU51bWJlcj48L01vYmlsZVRlbGVwaG9uZU51bWJlcj48QnVzaW5lc3NGYXhOdW1iZXI+PC9CdXNpbmVzc0ZheE51bWJlcj48Sm9iVGl0bGU+PC9Kb2JUaXRsZT48Q29tcGFueU5hbWU+PC9Db21wYW55TmFtZT48Qm9keT48L0JvZHk+PExhc3ROYW1lPnNlcnZlcjI8L0xhc3ROYW1lPjxGaXJzdE5hbWU+PC9GaXJzdE5hbWU+PE1pZGRsZU5hbWU+PC9NaWRkbGVOYW1lPjxUaXRsZT48L1RpdGxlPjxTdWZmaXg+PC9TdWZmaXg+PEhvbWVBZGRyZXNzU3RyZWV0PjwvSG9tZUFkZHJlc3NTdHJlZXQ+PEhvbWVBZGRyZXNzQ2l0eT48L0hvbWVBZGRyZXNzQ2l0eT48SG9tZUFkZHJlc3NTdGF0ZT48L0hvbWVBZGRyZXNzU3RhdGU+PEhvbWVBZGRyZXNzUG9zdGFsQ29kZT48L0hvbWVBZGRyZXNzUG9zdGFsQ29kZT48SG9tZUFkZHJlc3NDb3VudHJ5PjwvSG9tZUFkZHJlc3NDb3VudHJ5PjxIb21lQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9Ib21lQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PFdvcmtBZGRyZXNzU3RyZWV0PldvcmsgYWRkcmVzczwvV29ya0FkZHJlc3NTdHJlZXQ+PFdvcmtBZGRyZXNzQ2l0eT48L1dvcmtBZGRyZXNzQ2l0eT48V29ya0FkZHJlc3NTdGF0ZT48L1dvcmtBZGRyZXNzU3RhdGU+PFdvcmtBZGRyZXNzUG9zdGFsQ29kZT48L1dvcmtBZGRyZXNzUG9zdGFsQ29kZT48V29ya0FkZHJlc3NDb3VudHJ5PjwvV29ya0FkZHJlc3NDb3VudHJ5PjxXb3JrQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9Xb3JrQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9jb250YWN0Pg==</Data></Item></Add><Replace><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifc</Type></Meta><Item><Target><LocURI>83899317</LocURI></Target><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PD94bWwgdmVyc2lvbj0iMS4wIj8+PGNvbnRhY3Q+PEZpbGVBcz5DbGllbnQxczE8L0ZpbGVBcz48RW1haWwxQWRkcmVzcz5jbGllbnRAZXhhbXBsZS5jb208L0VtYWlsMUFkZHJlc3M+PEVtYWlsMUFkZHJlc3NUeXBlPlNNVFA8L0VtYWlsMUFkZHJlc3NUeXBlPjxOaWNrTmFtZT48L05pY2tOYW1lPjxIb21lVGVsZXBob25lTnVtYmVyPjwvSG9tZVRlbGVwaG9uZU51bWJlcj48QnVzaW5lc3NUZWxlcGhvbmVOdW1iZXI+NTU1LTEyMzwvQnVzaW5lc3NUZWxlcGhvbmVOdW1iZXI+PE1vYmlsZVRlbGVwaG9uZU51bWJlcj48L01vYmlsZVRlbGVwaG9uZU51bWJlcj48QnVzaW5lc3NGYXhOdW1iZXI+PC9CdXNpbmVzc0ZheE51bWJlcj48Sm9iVGl0bGU+PC9Kb2JUaXRsZT48Q29tcGFueU5hbWU+Q29tcGFueTwvQ29tcGFueU5hbWU+PEJvZHk+PC9Cb2R5PjxMYXN0TmFtZT5DbGllbnQxczE8L0xhc3ROYW1lPjxGaXJzdE5hbWU+PC9GaXJzdE5hbWU+PE1pZGRsZU5hbWU+PC9NaWRkbGVOYW1lPjxUaXRsZT48L1RpdGxlPjxTdWZmaXg+PC9TdWZmaXg+PEhvbWVBZGRyZXNzU3RyZWV0PjwvSG9tZUFkZHJlc3NTdHJlZXQ+PEhvbWVBZGRyZXNzQ2l0eT48L0hvbWVBZGRyZXNzQ2l0eT48SG9tZUFkZHJlc3NTdGF0ZT48L0hvbWVBZGRyZXNzU3RhdGU+PEhvbWVBZGRyZXNzUG9zdGFsQ29kZT48L0hvbWVBZGRyZXNzUG9zdGFsQ29kZT48SG9tZUFkZHJlc3NDb3VudHJ5PjwvSG9tZUFkZHJlc3NDb3VudHJ5PjxIb21lQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9Ib21lQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PFdvcmtBZGRyZXNzU3RyZWV0PjwvV29ya0FkZHJlc3NTdHJlZXQ+PFdvcmtBZGRyZXNzQ2l0eT48L1dvcmtBZGRyZXNzQ2l0eT48V29ya0FkZHJlc3NTdGF0ZT48L1dvcmtBZGRyZXNzU3RhdGU+PFdvcmtBZGRyZXNzUG9zdGFsQ29kZT48L1dvcmtBZGRyZXNzUG9zdGFsQ29kZT48V29ya0FkZHJlc3NDb3VudHJ5PjwvV29ya0FkZHJlc3NDb3VudHJ5PjxXb3JrQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9Xb3JrQWRkcmVzc1Bvc3RPZmZpY2VCb3g+PC9jb250YWN0Pg==</Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_23.xml
new file mode 100644 (file)
index 0000000..88662aa
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430116</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>5</CmdRef><Cmd>Map</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..657c838
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430524</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>26430116</Last><Next>26430524</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160775641</Last><Next>1160775756</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..68965cf
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430524</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..dbac288
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430524</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_33.xml
new file mode 100644 (file)
index 0000000..d0f60d6
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430524</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..e9f4bee
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430854</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>26430524</Last><Next>26430854</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160775756</Last><Next>1160775842</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..2caf489
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430854</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>contacts</TargetRef><SourceRef>contact</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><SourceRef>83899317</SourceRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><SourceRef>151008046</SourceRef><Data>200</Data></Status><Get><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..172add5
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430854</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>contact</LocURI></Target><Source><LocURI>contacts</LocURI></Source><Delete><CmdID>5</CmdID><Item><Target><LocURI>251660424</LocURI></Target></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_contacts/syncml_server_43.xml
new file mode 100644 (file)
index 0000000..146d0c5
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>26430854</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..7551c8a
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182112</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Meta><Anchor><Last>26433678</Last>
+<Next>28182112</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..5bf19ac
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182112</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160816429</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Replace><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifn</Type>
+</Meta>
+<Item><Source><LocURI>Y2xpZW50MS5wd2k=</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bm90ZT48U3ViamVjdD5jbGllbnQxPC9TdWJqZWN0PjxCb2R5PmNsaWVudDENCsOcYmVyc2ljaHQNCjwvQm9keT48RGF0ZT4yMDA2MTAxNFQwOTAwNTNaPC9EYXRlPjwvbm90ZT4=</Data>
+</Item>
+</Replace>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..fd4502d
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182112</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_13.xml
new file mode 100644 (file)
index 0000000..157a378
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182112</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061013235622.3a6dszb96so4@192.168.60.3</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>4</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061013235622.3a6dszb96so4@192.168.60.3</LocURI>
+</Target>
+<Source><LocURI>c2VydmVyMS5wd2k=</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..6ed2616
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182466</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Meta><Anchor><Last>28182112</Last>
+<Next>28182466</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..5c592ba
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182466</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160816525</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Add><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifn</Type>
+</Meta>
+<Item><Source><LocURI>Y2xpZW50Mi5wd2k=</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bm90ZT48U3ViamVjdD5jbGllbnQyPC9TdWJqZWN0PjxCb2R5PmNsaWVudDINClRlc3QNCjwvQm9keT48RGF0ZT4yMDA2MTAxNFQwOTAyMzBaPC9EYXRlPjwvbm90ZT4=</Data>
+</Item>
+<Item><Source><LocURI>c2VydmVyMWMxLnB3aQ==</LocURI>
+</Source>
+<Meta><Format>b64</Format>
+</Meta>
+<Data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bm90ZT48U3ViamVjdD5zZXJ2ZXIxYzE8L1N1YmplY3Q+PEJvZHk+c2VydmVyMQ0Kw5xiZXJzaWNodA0KPC9Cb2R5PjxEYXRlPjIwMDYxMDE0VDA5MDIzMFo8L0RhdGU+PC9ub3RlPg==</Data>
+</Item>
+</Add>
+<Delete><CmdID>5</CmdID>
+<Meta><Type>text/x-s4j-sifn</Type>
+</Meta>
+<Item><Source><LocURI>c2VydmVyMS5wd2k=</LocURI>
+</Source>
+</Item>
+</Delete>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..b652033
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182466</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_23.xml
new file mode 100644 (file)
index 0000000..28e12bd
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182466</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Add</Cmd>
+<Data>201</Data>
+<Item><Source><LocURI>20061014110154.6ro4zfnmes0s@trinity.wg.de</LocURI>
+</Source>
+</Item>
+</Status>
+<Status><CmdID>4</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>6</CmdRef>
+<Cmd>Replace</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>Y2xpZW50MS5wd2k=</LocURI>
+</Source>
+</Item>
+</Status>
+<Map><CmdID>5</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<MapItem><Target><LocURI>20061014110154.6ro4zfnmes0s@trinity.wg.de</LocURI>
+</Target>
+<Source><LocURI>c2VydmVyMi5wd2k=</LocURI>
+</Source>
+</MapItem>
+</Map>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..365fd04
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182518</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Meta><Anchor><Last>28182466</Last>
+<Next>28182518</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..4488c5a
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182518</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160816553</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..318e940
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182518</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_33.xml
new file mode 100644 (file)
index 0000000..e216338
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182518</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..5299ec4
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182658</SessionID>
+<MsgID>1</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Cred><Meta><Format>b64</Format>
+<Type>syncml:auth-basic</Type>
+</Meta>
+<Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data>
+</Cred>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Alert><CmdID>1</CmdID>
+<Data>200</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Meta><Anchor><Last>28182518</Last>
+<Next>28182658</Next>
+</Anchor>
+</Meta>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..de42b96
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182658</SessionID>
+<MsgID>2</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>1</MsgRef>
+<CmdRef>3</CmdRef>
+<Cmd>Alert</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+<Item><Data><Anchor><Next>1160816582</Next>
+</Anchor>
+</Data>
+</Item>
+</Status>
+<Sync><CmdID>3</CmdID>
+<Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+<Delete><CmdID>4</CmdID>
+<Meta><Type>text/x-s4j-sifn</Type>
+</Meta>
+<Item><Source><LocURI>c2VydmVyMi5wd2k=</LocURI>
+</Source>
+</Item>
+</Delete>
+</Sync>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..cd92df5
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182658</SessionID>
+<MsgID>3</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>2</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Alert><CmdID>2</CmdID>
+<Data>222</Data>
+<Item><Target><LocURI>notes</LocURI>
+</Target>
+<Source><LocURI>note</LocURI>
+</Source>
+</Item>
+</Alert>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_client_43.xml
new file mode 100644 (file)
index 0000000..3eacc9b
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SyncML>
+<SyncHdr><VerDTD>1.1</VerDTD>
+<VerProto>SyncML/1.1</VerProto>
+<SessionID>28182658</SessionID>
+<MsgID>4</MsgID>
+<Target><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI>
+</Target>
+<Source><LocURI>fwm-0E232B741AFE0</LocURI>
+</Source>
+<Meta><MaxMsgSize>16384</MaxMsgSize>
+</Meta>
+</SyncHdr>
+<SyncBody><Status><CmdID>1</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>0</CmdRef>
+<Cmd>SyncHdr</Cmd>
+<TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef>
+<SourceRef>fwm-0E232B741AFE0</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>2</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>4</CmdRef>
+<Cmd>Sync</Cmd>
+<TargetRef>notes</TargetRef>
+<SourceRef>note</SourceRef>
+<Data>200</Data>
+</Status>
+<Status><CmdID>3</CmdID>
+<MsgRef>3</MsgRef>
+<CmdRef>5</CmdRef>
+<Cmd>Delete</Cmd>
+<Data>200</Data>
+<Item><Source><LocURI>Y2xpZW50Mi5wd2k=</LocURI>
+</Source>
+</Item>
+</Status>
+<Final></Final>
+</SyncBody>
+</SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_10.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..f7fee5a
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182112</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>508</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>26433678</Last><Next>28182112</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>201</Data><Item><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1160816429</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_11.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..564d03a
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182112</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Replace</Cmd><SourceRef>Y2xpZW50MS5wd2k=</SourceRef><Data>201</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_12.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..69450dd
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182112</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifn</Type></Meta><Item><Source><LocURI>20061013235622.3a6dszb96so4@192.168.60.3</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PG5vdGU+PEJvZHk+c2VydmVyMQ0Kw5xiZXJzaWNodDwvQm9keT48Q2F0ZWdvcmllcz5NeSBDYXRlZ29yeTwvQ2F0ZWdvcmllcz48U3ViamVjdD5zZXJ2ZXIxPC9TdWJqZWN0PjxDb2xvcj48L0NvbG9yPjxIZWlnaHQ+PC9IZWlnaHQ+PFdpZHRoPjwvV2lkdGg+PExlZnQ+PC9MZWZ0PjxUb3A+PC9Ub3A+PC9ub3RlPg==</Data></Item></Add></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_13.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_13.xml
new file mode 100644 (file)
index 0000000..016792c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182112</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>4</CmdRef><Cmd>Map</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_20.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..854e63e
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182466</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>28182112</Last><Next>28182466</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160816429</Last><Next>1160816525</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_21.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..bcd4ddc
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182466</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>Y2xpZW50Mi5wd2k=</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>c2VydmVyMWMxLnB3aQ==</SourceRef><Data>201</Data></Status><Status><CmdID>5</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Delete</Cmd><SourceRef>c2VydmVyMS5wd2k=</SourceRef><Data>200</Data></Status><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_22.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..cef58ee
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182466</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifn</Type></Meta><Item><Source><LocURI>20061014110154.6ro4zfnmes0s@trinity.wg.de</LocURI></Source><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PG5vdGU+PEJvZHk+c2VydmVyMg0KU2VydmVyIFRlc3Q8L0JvZHk+PFN1YmplY3Q+c2VydmVyMjwvU3ViamVjdD48Q29sb3I+PC9Db2xvcj48SGVpZ2h0PjwvSGVpZ2h0PjxXaWR0aD48L1dpZHRoPjxMZWZ0PjwvTGVmdD48VG9wPjwvVG9wPjwvbm90ZT4=</Data></Item></Add><Replace><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">text/x-s4j-sifn</Type></Meta><Item><Target><LocURI>Y2xpZW50MS5wd2k=</LocURI></Target><Meta><Format xmlns="syncml:metinf">b64</Format></Meta><Data>PG5vdGU+PEJvZHk+Y2xpZW50MXMxDQrDnGJlcnNpY2h0PC9Cb2R5PjxTdWJqZWN0PmNsaWVudDFzMTwvU3ViamVjdD48Q29sb3I+PC9Db2xvcj48SGVpZ2h0PjwvSGVpZ2h0PjxXaWR0aD48L1dpZHRoPjxMZWZ0PjwvTGVmdD48VG9wPjwvVG9wPjwvbm90ZT4=</Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_23.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_23.xml
new file mode 100644 (file)
index 0000000..62f32ba
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182466</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>4</MsgRef><CmdRef>5</CmdRef><Cmd>Map</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_30.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..a30c947
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182518</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>28182466</Last><Next>28182518</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160816525</Last><Next>1160816553</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_31.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..507daa4
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182518</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_32.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..9759331
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182518</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_33.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_33.xml
new file mode 100644 (file)
index 0000000..298ba3d
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182518</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_40.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..dcd95a1
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182658</SessionID><MsgID>1</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName></LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Alert</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>28182518</Last><Next>28182658</Next></Anchor></Data></Item></Status><Alert><CmdID>3</CmdID><Data>200</Data><Item><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1160816553</Last><Next>1160816582</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_41.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..60a6804
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182658</SessionID><MsgID>2</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>notes</TargetRef><SourceRef>note</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Delete</Cmd><SourceRef>c2VydmVyMi5wd2k=</SourceRef><Data>200</Data></Status><Get><CmdID>4</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_42.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..263110c
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182658</SessionID><MsgID>3</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><Data>200</Data></Status><Get><CmdID>3</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Sync><CmdID>4</CmdID><Target><LocURI>note</LocURI></Target><Source><LocURI>notes</LocURI></Source><Delete><CmdID>5</CmdID><Item><Target><LocURI>Y2xpZW50Mi5wd2k=</LocURI></Target></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_43.xml b/framework/SyncML/tests/testcase_sync4j_pocketpc_notes/syncml_server_43.xml
new file mode 100644 (file)
index 0000000..7f143f9
--- /dev/null
@@ -0,0 +1 @@
+<SyncML xmlns="syncml:syncml1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>28182658</SessionID><MsgID>4</MsgID><Target><LocURI>fwm-0E232B741AFE0</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://192.168.60.3/headhorde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>4</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://192.168.60.3/headhorde/rpc.php</TargetRef><SourceRef>fwm-0E232B741AFE0</SourceRef><Data>200</Data></Status><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_10.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_10.xml
new file mode 100644 (file)
index 0000000..1708b7d
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>1</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Cred><Meta><Format xmlns="syncml:metinf">b64</Format><Type xmlns="syncml:metinf">syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Put><CmdID>1</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>Synthesis AG</Man><Mod>SySync Client PalmOS STD</Mod><OEM>Synthesis AG</OEM><FwV>v. 5.4.0.23</FwV><SwV>2.5.0.46</SwV><HwV>0</HwV><DevID>SERIALNUMBER</DevID><DevTyp>pda</DevTyp><SupportNumberOfChanges></SupportNumberOfChanges><SupportLargeObjs></SupportLargeObjs><DataStore><SourceRef>./tasks</SourceRef><MaxGUIDSize>64</MaxGUIDSize><Rx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx-Pref><Tx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx-Pref><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><CTCap><CTType>text/x-vcalendar</CTType><PropName>BEGIN</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>END</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>VERSION</PropName><ValEnum>1.0</ValEnum><PropName>SUMMARY</PropName><PropName>CATEGORIES</PropName><PropName>CLASS</PropName><PropName>DESCRIPTION</PropName><PropName>DTSTART</PropName><PropName>DTEND</PropName><PropName>RRULE</PropName><PropName>EXDATE</PropName><PropName>AALARM</PropName><PropName>DALARM</PropName><PropName>DUE</PropName><PropName>PRIORITY</PropName><PropName>STATUS</PropName></CTCap></DevInf></Data></Item></Put><Get><CmdID>2</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Alert><CmdID>3</CmdID><Data>201</Data><Item><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Next>20060722T215039Z</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_11.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_11.xml
new file mode 100644 (file)
index 0000000..20b4a46
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>2</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>4</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Alert</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>1153569058</Next></Anchor></Data></Item></Status><Sync><CmdID>4</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Replace><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>1798145</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:client1
+CLASS:PUBLIC
+DESCRIPTION:
+PRIORITY:1
+STATUS:NEEDS ACTION
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_12.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_12.xml
new file mode 100644 (file)
index 0000000..4c7f416
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>3</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Add</Cmd><SourceRef>20060722134917.76iotty9niww@voltaire.local</SourceRef><Data>201</Data></Status><Map><CmdID>5</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><MapItem><Target><LocURI>20060722134917.76iotty9niww@voltaire.local</LocURI></Target><Source><LocURI>1798146</LocURI></Source></MapItem></Map><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_20.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_20.xml
new file mode 100644 (file)
index 0000000..07a1715
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>1</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Cred><Meta><Format xmlns="syncml:metinf">b64</Format><Type xmlns="syncml:metinf">syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Get><CmdID>1</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Alert><CmdID>2</CmdID><Data>200</Data><Item><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>20060722T215039Z</Last><Next>20060722T215204Z</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_21.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_21.xml
new file mode 100644 (file)
index 0000000..4a2e2ad
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>2</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>5</CmdRef><Cmd>Alert</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>1153569142</Next></Anchor></Data></Item></Status><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>Synthesis AG</Man><Mod>SySync Client PalmOS STD</Mod><OEM>Synthesis AG</OEM><FwV>v. 5.4.0.23</FwV><SwV>2.5.0.46</SwV><HwV>0</HwV><DevID>SERIALNUMBER</DevID><DevTyp>pda</DevTyp><SupportNumberOfChanges></SupportNumberOfChanges><SupportLargeObjs></SupportLargeObjs><DataStore><SourceRef>./tasks</SourceRef><MaxGUIDSize>64</MaxGUIDSize><Rx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx-Pref><Tx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx-Pref><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><CTCap><CTType>text/x-vcalendar</CTType><PropName>BEGIN</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>END</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>VERSION</PropName><ValEnum>1.0</ValEnum><PropName>SUMMARY</PropName><PropName>CATEGORIES</PropName><PropName>CLASS</PropName><PropName>DESCRIPTION</PropName><PropName>DTSTART</PropName><PropName>DTEND</PropName><PropName>RRULE</PropName><PropName>EXDATE</PropName><PropName>AALARM</PropName><PropName>DALARM</PropName><PropName>DUE</PropName><PropName>PRIORITY</PropName><PropName>STATUS</PropName></CTCap></DevInf></Data></Item></Results><Sync><CmdID>6</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Replace><CmdID>7</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>1798146</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:server1c1
+CLASS:PUBLIC
+DESCRIPTION:
+PRIORITY:2
+STATUS:NEEDS ACTION
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace><Replace><CmdID>8</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>1798147</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+BEGIN:VTODO
+SUMMARY:client1
+CLASS:PUBLIC
+DESCRIPTION:
+PRIORITY:1
+STATUS:NEEDS ACTION
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_22.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_22.xml
new file mode 100644 (file)
index 0000000..6c91695
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>3</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Sync</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Add</Cmd><SourceRef>20060722135201.11hupchy2s80@voltaire.local</SourceRef><Data>201</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>7</CmdRef><Cmd>Replace</Cmd><TargetRef>1798145</TargetRef><Data>200</Data></Status><Map><CmdID>6</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><MapItem><Target><LocURI>20060722135201.11hupchy2s80@voltaire.local</LocURI></Target><Source><LocURI>1798148</LocURI></Source></MapItem></Map><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_30.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_30.xml
new file mode 100644 (file)
index 0000000..f93e6eb
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>1</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Cred><Meta><Format xmlns="syncml:metinf">b64</Format><Type xmlns="syncml:metinf">syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Get><CmdID>1</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Alert><CmdID>2</CmdID><Data>200</Data><Item><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>20060722T215204Z</Last><Next>20060722T215217Z</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_31.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_31.xml
new file mode 100644 (file)
index 0000000..7541f14
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>2</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>5</CmdRef><Cmd>Alert</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>1153569155</Next></Anchor></Data></Item></Status><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>Synthesis AG</Man><Mod>SySync Client PalmOS STD</Mod><OEM>Synthesis AG</OEM><FwV>v. 5.4.0.23</FwV><SwV>2.5.0.46</SwV><HwV>0</HwV><DevID>SERIALNUMBER</DevID><DevTyp>pda</DevTyp><SupportNumberOfChanges></SupportNumberOfChanges><SupportLargeObjs></SupportLargeObjs><DataStore><SourceRef>./tasks</SourceRef><MaxGUIDSize>64</MaxGUIDSize><Rx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx-Pref><Tx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx-Pref><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><CTCap><CTType>text/x-vcalendar</CTType><PropName>BEGIN</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>END</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>VERSION</PropName><ValEnum>1.0</ValEnum><PropName>SUMMARY</PropName><PropName>CATEGORIES</PropName><PropName>CLASS</PropName><PropName>DESCRIPTION</PropName><PropName>DTSTART</PropName><PropName>DTEND</PropName><PropName>RRULE</PropName><PropName>EXDATE</PropName><PropName>AALARM</PropName><PropName>DALARM</PropName><PropName>DUE</PropName><PropName>PRIORITY</PropName><PropName>STATUS</PropName></CTCap></DevInf></Data></Item></Results><Sync><CmdID>6</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_32.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_32.xml
new file mode 100644 (file)
index 0000000..71aaf8e
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>3</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>3</CmdRef><Cmd>Sync</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_40.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_40.xml
new file mode 100644 (file)
index 0000000..b00b9c7
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>1</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Cred><Meta><Format xmlns="syncml:metinf">b64</Format><Type xmlns="syncml:metinf">syncml:auth-basic</Type></Meta><Data>c3luY21sdGVzdDpzeW5jbWx0ZXN0</Data></Cred><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Get><CmdID>1</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Alert><CmdID>2</CmdID><Data>200</Data><Item><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>20060722T215217Z</Last><Next>20060722T215457Z</Next></Anchor></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_41.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_41.xml
new file mode 100644 (file)
index 0000000..f2e320a
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>2</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>5</CmdRef><Cmd>Alert</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>1153569316</Next></Anchor></Data></Item></Status><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>6</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>Synthesis AG</Man><Mod>SySync Client PalmOS STD</Mod><OEM>Synthesis AG</OEM><FwV>v. 5.4.0.23</FwV><SwV>2.5.0.46</SwV><HwV>0</HwV><DevID>SERIALNUMBER</DevID><DevTyp>pda</DevTyp><SupportNumberOfChanges></SupportNumberOfChanges><SupportLargeObjs></SupportLargeObjs><DataStore><SourceRef>./tasks</SourceRef><MaxGUIDSize>64</MaxGUIDSize><Rx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx-Pref><Tx-Pref><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx-Pref><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><CTCap><CTType>text/x-vcalendar</CTType><PropName>BEGIN</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>END</PropName><ValEnum>VCALENDAR</ValEnum><ValEnum>VEVENT</ValEnum><ValEnum>VTODO</ValEnum><PropName>VERSION</PropName><ValEnum>1.0</ValEnum><PropName>SUMMARY</PropName><PropName>CATEGORIES</PropName><PropName>CLASS</PropName><PropName>DESCRIPTION</PropName><PropName>DTSTART</PropName><PropName>DTEND</PropName><PropName>RRULE</PropName><PropName>EXDATE</PropName><PropName>AALARM</PropName><PropName>DALARM</PropName><PropName>DUE</PropName><PropName>PRIORITY</PropName><PropName>STATUS</PropName></CTCap></DevInf></Data></Item></Results><Sync><CmdID>6</CmdID><Target><LocURI>tasks</LocURI></Target><Source><LocURI>./tasks</LocURI></Source><Delete><CmdID>7</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>1798148</LocURI></Source></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_42.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_client_42.xml
new file mode 100644 (file)
index 0000000..c0b9cf9
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>3</MsgID><Target><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Target><Source><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Source><Meta><MaxMsgSize xmlns="syncml:metinf">10000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">64000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>SERIALNUMBER</TargetRef><SourceRef>http://voltaire.local/horde/rpc.php</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>./tasks</TargetRef><SourceRef>tasks</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Delete</Cmd><TargetRef>1798147</TargetRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_10.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_10.xml
new file mode 100644 (file)
index 0000000..4623e4b
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>1</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Put</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>voltaire.local</DevID><DevTyp>server</DevTyp><SupportLargeObjs></SupportLargeObjs><SupportNumberOfChanges></SupportNumberOfChanges><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>5</CmdID><MsgRef>1</MsgRef><CmdRef>3</CmdRef><Cmd>Alert</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Next>20060722T215039Z</Next></Anchor></Data></Item></Status><Alert><CmdID>6</CmdID><Data>201</Data><Item><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>0</Last><Next>1153569058</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_11.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_11.xml
new file mode 100644 (file)
index 0000000..a33b3d9
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>2</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>4</CmdRef><Cmd>Sync</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Replace</Cmd><SourceRef>1798145</SourceRef><Data>201</Data></Status><Sync><CmdID>4</CmdID><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><NumberOfChanges>1</NumberOfChanges><Add><CmdID>5</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>20060722134917.76iotty9niww@voltaire.local</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:server1
+PRIORITY:2
+STATUS:NEEDS ACTION
+DCREATED:20060722T114917Z
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Add></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_12.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_12.xml
new file mode 100644 (file)
index 0000000..4e35f58
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>10</SessionID><MsgID>3</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>5</CmdRef><Cmd>Map</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_20.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_20.xml
new file mode 100644 (file)
index 0000000..fc0bc82
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>1</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>voltaire.local</DevID><DevTyp>server</DevTyp><SupportLargeObjs></SupportLargeObjs><SupportNumberOfChanges></SupportNumberOfChanges><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20060722T215039Z</Last><Next>20060722T215204Z</Next></Anchor></Data></Item></Status><Alert><CmdID>5</CmdID><Data>200</Data><Item><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1153569058</Last><Next>1153569142</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_21.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_21.xml
new file mode 100644 (file)
index 0000000..5538f8f
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>2</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Sync</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>7</CmdRef><Cmd>Replace</Cmd><SourceRef>1798146</SourceRef><Data>200</Data></Status><Status><CmdID>5</CmdID><MsgRef>2</MsgRef><CmdRef>8</CmdRef><Cmd>Replace</Cmd><SourceRef>1798147</SourceRef><Data>201</Data></Status><Sync><CmdID>6</CmdID><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><NumberOfChanges>2</NumberOfChanges><Add><CmdID>7</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Source><LocURI>20060722135201.11hupchy2s80@voltaire.local</LocURI></Source><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:server2
+PRIORITY:2
+STATUS:NEEDS ACTION
+DCREATED:20060722T115201Z
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Add><Replace><CmdID>8</CmdID><Meta><Type xmlns="syncml:metinf">text/x-vcalendar</Type></Meta><Item><Target><LocURI>1798145</LocURI></Target><Data><![CDATA[BEGIN:VCALENDAR
+VERSION:1.0
+PRODID:-//The Horde Project//Nag H3 (2.2-cvs)//EN
+METHOD:PUBLISH
+BEGIN:VTODO
+ORGANIZER:syncmltest
+SUMMARY:client1s1
+PRIORITY:1
+DTSTART:20071201T230000Z
+DUE:20071205T000000
+STATUS:NEEDS ACTION
+DCREATED:20060722T115059Z
+LAST-MODIFIED:20060722T115132Z
+END:VTODO
+END:VCALENDAR
+]]></Data></Item></Replace></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_22.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_22.xml
new file mode 100644 (file)
index 0000000..b9b3779
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>15</SessionID><MsgID>3</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>3</MsgRef><CmdRef>6</CmdRef><Cmd>Map</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_30.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_30.xml
new file mode 100644 (file)
index 0000000..9b26d73
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>1</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>voltaire.local</DevID><DevTyp>server</DevTyp><SupportLargeObjs></SupportLargeObjs><SupportNumberOfChanges></SupportNumberOfChanges><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20060722T215204Z</Last><Next>20060722T215217Z</Next></Anchor></Data></Item></Status><Alert><CmdID>5</CmdID><Data>200</Data><Item><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1153569142</Last><Next>1153569155</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_31.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_31.xml
new file mode 100644 (file)
index 0000000..788e8a9
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>2</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Sync</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Sync><CmdID>4</CmdID><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><NumberOfChanges>0</NumberOfChanges></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_32.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_32.xml
new file mode 100644 (file)
index 0000000..c208131
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>20</SessionID><MsgID>3</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_40.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_40.xml
new file mode 100644 (file)
index 0000000..54049ca
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>1</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>1</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>212</Data></Status><Status><CmdID>2</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Cmd>Get</Cmd><TargetRef>./devinf11</TargetRef><Data>200</Data></Status><Results><CmdID>3</CmdID><MsgRef>1</MsgRef><CmdRef>1</CmdRef><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Source><LocURI>./devinf11</LocURI></Source><Data><DevInf xmlns="syncml:devinf"><VerDTD>1.1</VerDTD><Man>The Horde Project (http://www.horde.org/)</Man><DevID>voltaire.local</DevID><DevTyp>server</DevTyp><SupportLargeObjs></SupportLargeObjs><SupportNumberOfChanges></SupportNumberOfChanges><DataStore><SourceRef>notes</SourceRef><Rx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Rx-Pref><Rx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/x-vnote</CTType><VerCT>1.1</VerCT></Tx-Pref><Tx><CTType>text/plain</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>contacts</SourceRef><Rx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Rx><Tx-Pref><CTType>text/x-vcard</CTType><VerCT>3.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcard</CTType><VerCT>2.1</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>tasks</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore><DataStore><SourceRef>calendar</SourceRef><Rx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Rx-Pref><Rx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Rx><Tx-Pref><CTType>text/calendar</CTType><VerCT>2.0</VerCT></Tx-Pref><Tx><CTType>text/x-vcalendar</CTType><VerCT>1.0</VerCT></Tx><SyncCap><SyncType>1</SyncType><SyncType>2</SyncType><SyncType>3</SyncType><SyncType>4</SyncType><SyncType>5</SyncType><SyncType>6</SyncType></SyncCap></DataStore></DevInf></Data></Item></Results><Status><CmdID>4</CmdID><MsgRef>1</MsgRef><CmdRef>2</CmdRef><Cmd>Alert</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data><Item><Data><Anchor xmlns="syncml:metinf"><Last>20060722T215217Z</Last><Next>20060722T215457Z</Next></Anchor></Data></Item></Status><Alert><CmdID>5</CmdID><Data>200</Data><Item><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><Meta><Anchor xmlns="syncml:metinf"><Last>1153569155</Last><Next>1153569316</Next></Anchor><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></Item></Alert><Get><CmdID>6</CmdID><Meta><Type xmlns="syncml:metinf">application/vnd.syncml-devinf+xml</Type></Meta><Item><Target><LocURI>./devinf11</LocURI></Target></Item></Get><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_41.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_41.xml
new file mode 100644 (file)
index 0000000..30148ed
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>2</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>2</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Status><CmdID>2</CmdID><MsgRef>2</MsgRef><CmdRef>5</CmdRef><Cmd>Results</Cmd><SourceRef>./devinf11</SourceRef><Data>200</Data></Status><Status><CmdID>3</CmdID><MsgRef>2</MsgRef><CmdRef>6</CmdRef><Cmd>Sync</Cmd><TargetRef>tasks</TargetRef><SourceRef>./tasks</SourceRef><Data>200</Data></Status><Status><CmdID>4</CmdID><MsgRef>2</MsgRef><CmdRef>7</CmdRef><Cmd>Delete</Cmd><SourceRef>1798148</SourceRef><Data>200</Data></Status><Sync><CmdID>5</CmdID><Target><LocURI>./tasks</LocURI></Target><Source><LocURI>tasks</LocURI></Source><NumberOfChanges>1</NumberOfChanges><Delete><CmdID>6</CmdID><Item><Target><LocURI>1798147</LocURI></Target></Item></Delete></Sync><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_42.xml b/framework/SyncML/tests/testcase_synthesis_tasks/syncml_server_42.xml
new file mode 100644 (file)
index 0000000..b052e7b
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd"><SyncML xmlns="syncml:SYNCML1.1"><SyncHdr><VerDTD>1.1</VerDTD><VerProto>SyncML/1.1</VerProto><SessionID>25</SessionID><MsgID>3</MsgID><Target><LocURI>SERIALNUMBER</LocURI><LocName>syncmltest</LocName></Target><Source><LocURI>http://voltaire.local/horde/rpc.php</LocURI></Source><Meta><MaxMsgSize xmlns="syncml:metinf">1000000</MaxMsgSize><MaxObjSize xmlns="syncml:metinf">31000</MaxObjSize></Meta></SyncHdr><SyncBody><Status><CmdID>1</CmdID><MsgRef>3</MsgRef><CmdRef>0</CmdRef><Cmd>SyncHdr</Cmd><TargetRef>http://voltaire.local/horde/rpc.php</TargetRef><SourceRef>SERIALNUMBER</SourceRef><Data>200</Data></Status><Final></Final></SyncBody></SyncML>
\ No newline at end of file
diff --git a/framework/SyncML/tests/testpacket.php b/framework/SyncML/tests/testpacket.php
new file mode 100755 (executable)
index 0000000..328b6a2
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/php
+<?php
+/**
+ * Script to test individual SyncML messages.
+ *
+ * The scripts takes a single client message, either XML or WBXML encoded, and
+ * tries to parse it and generate a response message. It doesn't talk to any
+ * backend, so it's not able to test the actualy command being sent in the
+ * message. Its purpose is to make sure that SyncML messages are correctly and
+ * completely parsed and distributed into the business logic.
+ *
+ * $Horde: framework/SyncML/tests/testpacket.php,v 1.4 2009/06/09 23:23:46 slusarz Exp $
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.3.4
+ * @package SyncML
+ */
+
+require_once 'PEAR.php';
+require_once 'SyncML.php';
+
+class Backend extends SyncML_Backend {
+
+    var $_logLevel = PEAR_LOG_DEBUG;
+
+    function logMessage($message, $file, $line, $priority = PEAR_LOG_INFO)
+    {
+        parent::logMessage($message, $file, $line, $priority);
+        echo $this->_logtext;
+        $this->_logtext = '';
+    }
+
+    function logFile()
+    {
+    }
+
+    function _checkAuthentication($username)
+    {
+        return strlen($username) ? $username : true;
+    }
+
+    function setupState(&$state)
+    {
+        $state->user = 'dummyUser';
+        $state->authenticated = true;
+    }
+
+    function addEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        echo "Adding $cuid of $contentType to $databaseURI:\n$content\n";
+    }
+
+    function replaceEntry($databaseURI, $content, $contentType, $cuid)
+    {
+        echo "Replacing $cuid of $contentType in $databaseURI:\n$content\n";
+    }
+
+    function deleteEntry($databaseURI, $cuid)
+    {
+        echo "Deleting $cuid from $databaseURI\n";
+    }
+
+}
+
+if (!isset($argc)) {
+    die("argv/argc has to be enabled.\n");
+}
+if ($argc != 2) {
+    die('Usage: ' . basename($argv[0]) . " syncml_client_nn.[wb]xml\n");
+}
+
+$backend = new Backend(array());
+$sync = new SyncML_ContentHandler();
+$sync->debug = true;
+$sync->process(file_get_contents($argv[1]), strpos($argv[1], '.wbxml') ? 'application/vnd.syncml+wbxml' : 'application/vnd.syncml');
+$output = $sync->getOutput();
+if (function_exists('tidy_repair_string')) {
+    $output = tidy_repair_string($output, array('indent' => true, 'input-xml' => true, 'output-xml' => true));
+}
+echo $output, "\n";
+@session_destroy();
diff --git a/framework/SyncML/tests/testsync.php b/framework/SyncML/tests/testsync.php
new file mode 100755 (executable)
index 0000000..12a321c
--- /dev/null
@@ -0,0 +1,691 @@
+#!/usr/bin/php
+<?php
+/**
+ * Script to test the SyncML implementation.
+ *
+ * Takes a pre-recorded testcase, stuffs the data into the SyncML server, and
+ * then compares the output to see if it matches.
+ *
+ * See http://wiki.horde.org/SyncHowTo for a description how to create a test
+ * case.
+ *
+ * $$Horde: framework/SyncML/tests/testsync.php,v 1.26 2009/07/22 06:40:30 slusarz Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Karsten Fourmont <karsten@horde.org>
+ * @since   Horde 3.2
+ * @package SyncML
+ */
+
+/* Current limitations:
+ *
+ * - $service is set globally, so syncing multiple databases at once is not
+ *   dealt with: should be fixed easily by retrieving service using some
+ *   regular expression magic.
+ *
+ * - Limited to 3 messages per session style. This is more serious.
+ *
+ * - Currently the test case has to start with a slowsync. Maybe we can remove
+ *   this restriction and thus allow test cases with "production phones".
+ *   An idea to deal with this: make testsync.php work with *any* recorded
+ *   sessions:
+ *   - change any incoming auth to syncmltest:syncmltest
+ *   - identify twowaysync and create fake anchors for that */
+
+require_once 'SyncML.php';
+
+define('SYNCMLTEST_USERNAME', 'syncmltest');
+
+// Setup default backend parameters:
+$syncml_backend_driver = 'Horde';
+$syncml_backend_parms = array(
+    /* debug output to this dir, must be writeable be web server: */
+    'debug_dir' => '/tmp/sync',
+    /* log all (wb)xml packets received or sent to debug_dir: */
+    'debug_files' => true,
+    /* Log everything: */
+    'log_level' => PEAR_LOG_DEBUG);
+
+/* Get any options. */
+if (!isset($argv)) {
+    print_usage();
+}
+
+/* Get rid of the first arg which is the script name. */
+$this_script = array_shift($argv);
+
+while ($arg = array_shift($argv)) {
+    if ($arg == '--help') {
+        print_usage();
+    } elseif (strstr($arg, '--setup')) {
+        $testsetuponly = true;
+    } elseif (strstr($arg, '--url')) {
+        list(, $url) = explode('=', $arg);
+    } elseif (strstr($arg, '--dir')) {
+        list(, $dir) = explode('=', $arg);
+    } elseif (strstr($arg, '--dsn')) {
+        list(, $dsn) = explode('=', $arg);
+        $syncml_backend_parms['dsn'] = $dsn;
+        $syncml_backend_driver = 'Sql';
+    } elseif (strstr($arg, '--debug')) {
+        if (strstr($arg, '=') !== false) {
+            list(, $debuglevel) = explode('=', $arg);
+        } else {
+            $debuglevel = 5;
+        }
+    } else {
+        print_usage("Unrecognised option $arg");
+    }
+}
+
+require_once 'Log.php';
+require_once 'SyncML/Device.php';
+require_once 'SyncML/Device/Sync4j.php';
+require_once 'SyncML/Backend.php';
+
+/* Do Horde includes if test for horde backend: */
+if ($syncml_backend_driver == 'Horde') {
+    /* Syncml does its own session handling. */
+    $horde_authentication = 'none';
+    $horde_session_control = 'none';
+    require_once dirname(__FILE__) . '/../../../lib/core.php';
+
+    // Load the CLI environment - make sure there's no time limit, init
+    // some variables, etc.
+    $cli = Horde_Cli::singleton();
+    $cli->init();
+
+    require_once HORDE_BASE . '/lib/base.php';
+    Horde_String::setDefaultCharset('UTF-8');
+    Horde_Nls::setCharset('UTF-8');
+}
+
+if (!empty($testsetuponly)) {
+    $testbackend = SyncML_Backend::factory($syncml_backend_driver,
+                                           $syncml_backend_parms);
+    $testbackend->testSetup(SYNCMLTEST_USERNAME, 'syncmltest');
+    echo "Test setup for user syncmltest done. Now you can start to record a test case.\n";
+    exit(0);
+}
+
+/* Set this to true to skip cleanup after tests.  */
+$skipcleanup = false;
+
+/* mapping from LocUris to UIDs. Currently unused */
+$mapping_locuri2uid = array();
+
+/* The actual testing takes place her: */
+if (!empty($dir)) {
+    test($dir);
+} else {
+    $d = dir('./');
+    while (false !== ($entry = $d->read())) {
+        if (preg_match('/^testcase_/', $entry) && is_dir($d->path . $entry)) {
+            test($d->path . $entry);
+        }
+    }
+    $d->close();
+}
+
+/**
+ * Retrieves the reference data for one packet.
+ */
+function getServer($name, $number)
+{
+    if (!file_exists($name . '/syncml_server_' . $number . '.xml')) {
+        return false;
+    }
+    return file_get_contents($name . '/syncml_server_' . $number . '.xml');
+}
+
+/**
+ * Retrieves the client data to be sent to the server
+ */
+function getClient($name, $number)
+{
+    if (!file_exists($name . '/syncml_client_' . $number . '.xml')) {
+        return false;
+    }
+    return file_get_contents($name . '/syncml_client_' . $number . '.xml');
+}
+
+
+/**
+ * Compares $r and $ref.
+ *
+ * Exits if any nontrivial differences are found.
+ */
+function check($name, $r, $ref, $packetnum = 'unknown')
+{
+    $r   = trim(decodebase64data($r));
+    $ref = trim(decodebase64data($ref));
+
+    /* various tweaking: */
+    // case issues:
+    $search = array(
+        '| xmlns="syncml:SYNCML1.1"|i',
+        '|<DevID>.*?</DevID>|i',
+        '|<\?xml[^>]*>|i',
+        '|<!DOCTYPE[^>]*>|i',
+
+        /* Ignore timestamps used by various devices. */
+        '/(\r\n|\r|\n)DCREATED.*?(\r\n|\r|\n)/',
+        '/(\r\n|\r|\n)LAST-MODIFIED.*?(\r\n|\r|\n)/',
+        '/(\r\n|\r|\n)DTSTAMP.*?(\r\n|\r|\n)/',
+        '/(\r\n|\r|\n)X-WR-CALNAME.*?(\r\n|\r|\n)/',
+
+        /* Issues with priority, ignore for now. */
+        '/(\r\n|\r|\n)PRIORITY.*?(\r\n|\r|\n)/',
+
+        '|<Data>\s*(.*?)\s*</Data>|s',
+        '/\r/',
+        '/\n/');
+
+    $replace = array(
+        ' xmlns="syncml:SYNCML1.1"',
+        '<DevID>IGNORED</DevID>',
+        '',
+        '',
+
+        /* Ignore timestamps used by various devices. */
+        '$1',
+        '$1',
+        '$1',
+        '$1',
+
+        /* Issues with priority, ignore for now. */
+        '$1PRIORITY: IGNORED$2',
+
+        '<Data>$1</Data>',
+        '\r',
+        '\n');
+
+    $r   = preg_replace($search, $replace, $r);
+    $ref = preg_replace($search, $replace, $ref);
+
+    if (strcasecmp($r, $ref) !== 0) {
+        echo "Error in test case $name packet $packetnum\nReference:\n$ref\nResult:\n$r\n";
+        for($i = 0; $r[$i] == $ref[$i] && $i <= strlen($r); ++$i) {
+            // Noop.
+        }
+        echo "at position $i\n";
+        echo '"' . substr($ref, $i, 10) . '" vs. "' . substr($r, $i, 10) . "\"\n";
+        exit(1);
+    }
+}
+
+
+/**
+ * Simulates a call to the SyncML server by sending data to the server.
+ * Returns the result received from the server.
+ */
+function getResponse($data)
+{
+    if (!empty($GLOBALS['url'])) {
+        /* Call externally using curl. */
+        $tmpfile = tempnam('tmp','syncmltest');
+        $fh = fopen($tmpfile, 'w');
+        fwrite($fh, $data);
+        fclose($fh);
+        $output = shell_exec(sprintf('curl -s -H "Content-Type: application/vnd.syncml+xml" --data-binary @%s "%s"',
+                                     $tmpfile,
+                                     $GLOBALS['url']));
+        unlink($tmpfile);
+        return $output;
+    }
+
+    /* Create and setup the test backend */
+    $GLOBALS['backend'] = SyncML_Backend::factory(
+        $GLOBALS['syncml_backend_driver'],
+        $GLOBALS['syncml_backend_parms']);
+    $h = new SyncML_ContentHandler();
+    $response = $h->process($data, 'application/vnd.syncml+xml');
+    $GLOBALS['backend']->close();
+    return $response;
+}
+
+function getUIDs($data)
+{
+    // <LocURI>20060130082509.4nz5ng6sm9wk@127.0.0.1</LocURI>
+    if (!preg_match('|<Sync>.*</Sync>|s', $data, $m)) {
+        return array();
+    }
+    $data = $m[0];
+    // echo $data;
+    $count = preg_match_all('|(?<=<LocURI>)\d+[^<]*@[^<]*(?=</LocURI>)|is', $data, $m);
+    // if(count($m[0])>0) { var_dump($m[0]); }
+
+    return $m[0];
+}
+
+
+/* Decode sync4j base64 decoded data for readable debug outout. */
+function decodebase64data($s)
+{
+    return  preg_replace_callback('|(?<=<Data>)[0-9a-zA-Z\+\/=]{6,}(?=</Data>)|i',
+                                  create_function('$matches','return base64_decode($matches[0]);'),
+                                  $s);
+
+}
+
+function convertAnchors(&$ref,$r, $anchor = '')
+{
+    if ($anchor) {
+        $count = preg_match_all('|<Last>(\d+)</Last>|i', $ref, $m);
+        if ($count > 0 ) {
+            $temp = $m[1][$count-1];
+        }
+        $ref = str_replace("<Last>$temp</Last>", "<Last>$anchor</Last>" , $ref);
+    }
+    $count =  preg_match_all('|<Next>(\d+)</Next>|i', $r, $m);
+    if ($count > 0 ) {
+        $anchor = $m[1][$count-1];
+        $count = preg_match_all('|<Next>(\d+)</Next>|i', $ref, $m);
+        if ($count > 0 ) {
+            $temp = $m[1][$count-1];
+            $ref = str_replace("<Next>$temp</Next>", "<Next>$anchor</Next>" , $ref);
+        }
+    } else {
+        $anchor = '';
+    }
+
+    return $anchor;
+}
+
+/**
+ * Tests one sync session.
+ *
+ * Returns true on successful test and false on no (more) test data available
+ * for this $startnumber.  Exits if test fails.
+ */
+function testSession($name, $startnumber, &$anchor)
+{
+    global $debuglevel;
+
+    $uids = $refuids = array();
+
+    $number = $startnumber;
+    while ($ref = getServer($name, $number)) {
+        if ($debuglevel >= 2) {
+        }
+        testPre($name, $number);
+        $number++;
+    }
+
+    $number = $startnumber;
+    while ($ref = getServer($name, $number)) {
+        if ($debuglevel >= 2) {
+            echo "handling packet $number\n";
+        }
+
+        $c = str_replace($refuids, $uids, getClient($name, $number));
+        $resp = getResponse($c);
+
+        /* Set anchor from prev sync as last anchor: */
+        /* @TODO: this assumes startnumber in first packet */
+        if ($number == $startnumber) {
+            $anchor = convertAnchors($ref, $resp, $anchor);
+        }
+
+        $resp     = sortChanges($resp);
+        $ref      = sortChanges($ref);
+        $tuids    = getUIDs($resp);
+        $trefuids = getUIDs($ref);
+        $uids     = array_merge($uids, $tuids);
+        $refuids  = array_merge($refuids, $trefuids);
+        $ref      = str_replace($refuids, $uids, $ref);
+
+        parse_map($c);
+        check($name, $resp, $ref, $number);
+
+        $number++;
+    }
+
+    if ($number == $startnumber) {
+        // No packet found at all, end of test.
+        return false;
+    }
+
+    return true;
+}
+
+/**
+ * Parses and stores the map info sent by the client.
+ */
+function parse_map($content)
+{
+
+/* Example:
+<MapItem>
+<Target><LocURI>20060610121904.4svcwdpc5lkw@voltaire.local</LocURI></Target>
+<Source><LocURI>000000004FCBE97B738E984EAF085560B1DD2D50A4412000</LocURI></Source>
+</MapItem>
+*/
+
+    global $mapping_locuri2uid;
+    if (preg_match_all('|<MapItem>\s*<Target>\s*<LocURI>(.*?)</LocURI>.*?<Source>\s*<LocURI>(.*?)</LocURI>.*?</MapItem>|si', $content, $m, PREG_SET_ORDER)) {
+        foreach ($m as $c) {
+            $mapping_locuri2uid[$c[2]] = $c[1]; // store UID used by server
+        }
+    }
+
+}
+
+/**
+ * When a test case contains adds/modifies/deletes being sent to the server,
+ * these changes must be extracted from the test data and manually performed
+ * using the api to achieve the desired behaviour by the server
+ *
+ * @throws Horde_Exception
+ */
+function testPre($name, $number)
+{
+    global $debuglevel;
+
+    $ref0 = getClient($name, $number);
+
+    // Extract database (in horde: service).
+    if (preg_match('|<Alert>.*?<Target>\s*<LocURI>([^>]*)</LocURI>.*?</Alert>|si', $ref0, $m)) {
+        $GLOBALS['service'] = $m[1];
+    }
+
+    if (!preg_match('|<SyncHdr>.*?<Source>\s*<LocURI>(.*?)</LocURI>.*?</SyncHdr>|si', $ref0, $m)) {
+        echo $ref0;
+        throw new Horde_Exception('Unable to find device id');
+    }
+    $device_id = $m[1];
+
+    // Start backend session if not already done.
+    if ($GLOBALS['testbackend']->getSyncDeviceID() != $device_id) {
+        $GLOBALS['testbackend']->sessionStart($device_id, null, SYNCML_BACKENDMODE_TEST);
+    }
+
+    // This makes a login even when a logout has occured when the session got
+    // deleted.
+    $GLOBALS['testbackend']->setUser(SYNCMLTEST_USERNAME);
+
+    $ref1 = getServer($name, $number + 1);
+    if (!$ref1) {
+        return;
+    }
+
+    $ref1 = str_replace(array('<![CDATA[', ']]>', '<?xml version="1.0"?><!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd">'),
+                        '', $ref1);
+
+    // Check for Adds.
+    if (preg_match_all('|<Add>.*?<type[^>]*>(.*?)</type>.*?<LocURI[^>]*>(.*?)</LocURI>.*?<data[^>]*>(.*?)</data>.*?</Add|si', $ref1, $m, PREG_SET_ORDER)) {
+        foreach ($m as $c) {
+            list(, $contentType, $locuri, $data) = $c;
+            // Some Sync4j tweaking.
+            switch (Horde_String::lower($contentType)) {
+            case 'text/x-s4j-sifn' :
+                $data = SyncML_Device_sync4j::sif2vnote(base64_decode($data));
+                $contentType = 'text/x-vnote';
+                $service = 'notes';
+                break;
+
+            case 'text/x-s4j-sifc' :
+                $data = SyncML_Device_sync4j::sif2vcard(base64_decode($data));
+                $contentType = 'text/x-vcard';
+                $service = 'contacts';
+                break;
+
+            case 'text/x-s4j-sife' :
+                $data = SyncML_Device_sync4j::sif2vevent(base64_decode($data));
+                $contentType = 'text/calendar';
+                $service = 'calendar';
+                break;
+
+            case 'text/x-s4j-sift' :
+                $data = SyncML_Device_sync4j::sif2vtodo(base64_decode($data));
+                $contentType = 'text/calendar';
+                $service = 'tasks';
+                break;
+
+            case 'text/x-vcalendar':
+            case 'text/calendar':
+                if (preg_match('/(\r\n|\r|\n)BEGIN:\s*VTODO/', $data)) {
+                    $service = 'tasks';
+                } else {
+                    $service = 'calendar';
+                }
+                break;
+
+            default:
+                throw new Horde_Exception("Unable to find service for contentType=$contentType");
+            }
+
+            $result = $GLOBALS['testbackend']->addEntry($service, $data, $contentType);
+            if (is_a($result, 'PEAR_Error')) {
+                echo "error importing data into $service:\n$data\n";
+                throw new Horde_Exception($result);
+            }
+
+            if ($debuglevel >= 2) {
+                echo "simulated $service add of $result as $locuri!\n";
+                echo '   at ' . date('Y-m-d H:i:s') . "\n";
+                if ($debuglevel >= 10) {
+                    echo "data: $data\nsuid=$result\n";
+                }
+            }
+
+            // Store UID used by server.
+            $GLOBALS['mapping_locuri2uid'][$locuri] = $result;
+        }
+    }
+
+    // Check for Replaces.
+    if (preg_match_all('|<Replace>.*?<type[^>]*>(.*?)</type>.*?<LocURI[^>]*>(.*?)</LocURI>.*?<data[^>]*>(.*?)</data>.*?</Replace|si', $ref1, $m, PREG_SET_ORDER)) {
+        foreach ($m as $c) {
+            list(, $contentType, $locuri, $data) = $c;
+            // Some Sync4j tweaking.
+            switch (Horde_String::lower($contentType)) {
+            case 'sif/note' :
+            case 'text/x-s4j-sifn' :
+                $data = SyncML_Device_sync4j::sif2vnote(base64_decode($data));
+                $contentType = 'text/x-vnote';
+                $service = 'notes';
+                break;
+
+            case 'sif/contact' :
+            case 'text/x-s4j-sifc' :
+                $data = SyncML_Device_sync4j::sif2vcard(base64_decode($data));
+                $contentType = 'text/x-vcard';
+                $service = 'contacts';
+                break;
+
+            case 'sif/calendar' :
+            case 'text/x-s4j-sife' :
+                $data = SyncML_Device_sync4j::sif2vevent(base64_decode($data));
+                $contentType = 'text/calendar';
+                $service = 'calendar';
+                break;
+
+            case 'sif/task' :
+            case 'text/x-s4j-sift' :
+                $data = SyncML_Device_sync4j::sif2vtodo(base64_decode($data));
+                $contentType = 'text/calendar';
+                $service = 'tasks';
+                break;
+
+            case 'text/x-vcalendar':
+            case 'text/calendar':
+                if (preg_match('/(\r\n|\r|\n)BEGIN:\s*VTODO/', $data)) {
+                    $service = 'tasks';
+                } else {
+                    $service = 'calendar';
+                }
+                break;
+
+            default:
+                throw new Horde_Exception("Unable to find service for contentType=$contentType");
+            }
+
+            /* Get SUID. */
+            $suid = $GLOBALS['testbackend']->getSuid($service, $locuri);
+            if (empty($suid)) {
+                throw new Horde_Exception("Unable to find SUID for CUID $locuri for simulating replace");
+            }
+
+            $result = $GLOBALS['testbackend']->replaceEntry($service, $data, $contentType, $suid);
+            if (is_a($result, 'PEAR_Error')) {
+                echo "Error replacing data $locuri suid=$suid!\n";
+                throw new Horde_Exception($result);
+            }
+
+            if ($debuglevel >= 2) {
+                echo "simulated $service replace of $locuri suid=$suid!\n";
+                if ($debuglevel >= 10) {
+                    echo "data: $data\nnew id=$result\n";
+                }
+            }
+        }
+    }
+
+    // Check for Deletes.
+    // <Delete><CmdID>5</CmdID><Item><Target><LocURI>1798147</LocURI></Target></Item></Delete>
+    if (preg_match_all('|<Delete>.*?<Target>\s*<LocURI>(.*?)</LocURI>|si', $ref1, $m, PREG_SET_ORDER)) {
+        foreach ($m as $d) {
+            list(, $locuri) = $d;
+
+            /* Get SUID. */
+            $service = $GLOBALS['service'];
+            $suid = $GLOBALS['testbackend']->getSuid($service, $locuri);
+            if (empty($suid)) {
+                // Maybe we have a handletaskincalendar.
+                if ($service == 'calendar') {
+                    if ($debuglevel >= 2) {
+                        echo "special tasks delete...\n";
+                    }
+                    $service = 'tasks';
+                    $suid = $GLOBALS['testbackend']->getSuid($service, $locuri);
+                }
+            }
+            if (empty($suid)) {
+                throw new Horde_Exception("Unable to find SUID for CUID $locuri for simulating $service delete");
+            }
+
+            $result = $GLOBALS['testbackend']->deleteEntry($service, $suid);
+            // @TODO: simulate a delete by just faking some history data.
+            if (is_a($result, 'PEAR_Error')) {
+                echo "Error deleting data $locuri!";
+                throw new Horde_Exception($result);
+            }
+            if ($debuglevel >= 2) {
+                echo "simulated $service delete of $suid!\n";
+            }
+        }
+    }
+}
+
+/**
+ * Executes one test case.
+ *
+ * A test cases consists of various pre-recorded .xml packets in directory
+ * $name.
+ */
+function test($name)
+{
+    system($GLOBALS['this_script'] . ' --setup');
+    $GLOBALS['testbackend'] = SyncML_Backend::factory(
+        $GLOBALS['syncml_backend_driver'],
+        $GLOBALS['syncml_backend_parms']);
+    $GLOBALS['testbackend']->testStart(SYNCMLTEST_USERNAME, 'syncmltest');
+
+    $packetNum = 10;
+    $anchor = '';
+    while (testsession($name, $packetNum, $anchor) === true) {
+        $packetNum += 10;
+    }
+
+    /* Cleanup */
+    if (!$GLOBALS['skipcleanup']) {
+        $GLOBALS['testbackend'] ->testTearDown();
+    }
+
+    echo "testcase $name: passed\n";
+}
+
+
+/**
+ * We can't know in which ordeer changes (Add|Replace|Delete) changes are
+ * reported by the backend. One time it may list change1 and then change2,
+ * another time first change2 and then change1. So we just sort them to get
+ * a comparable result. The LocURIs must be ignored for the sort as we
+ * fake them during the test.
+ *
+ * @throws Horde_Exception
+ */
+function sortChanges($content)
+{
+    $bak = $content;
+
+    if (preg_match_all('!<(?:Add|Replace|Delete)>.*?</(?:Add|Replace|Delete)>!si', $content, $ma)) {
+        $eles = $ma[0];
+        preg_match('!^(.*?)<(?:Add|Replace|Delete)>.*</(?:Add|Replace|Delete)>(.*?)$!si', $content, $m);
+        //        var_dump($eles);
+        //        var_dump($m);
+        usort($eles, 'cmp');
+        $r = $m[1] . implode('',$eles) . $m[2];
+        if (strlen($r) != strlen($bak)) {
+            echo "error!\nbefore: $bak\nafter:  $r\n";
+            var_dump($m);
+            throw new Horde_Exception('failed');
+        }
+        // the CmdID may no longer fit. So we have to remove this:
+        $r = preg_replace('|<CmdID>[^<]*</CmdID>|','<CmdID>IGNORED</CmdID>', $r);
+        //echo 'sorted: ' . implode('',$eles) . "\n";
+        return $r;
+    }
+
+    return $content;
+}
+
+
+function cmp($a, $b)
+{
+    if (preg_match('|<Data>.*?</Data>|si', $a, $m)) {
+        $a = $m[0];
+        //echo "MATCH: $a\n";
+    } else {
+        $a = preg_replace('|<LocURI>.*?<LocURI>|s','', $a);
+    }
+    if (preg_match('|<Data>.*?</Data>|si', $b, $m)) {
+        $b = $m[0];
+    } else {
+        $b = preg_replace('|<LocURI>.*?<LocURI>|s','', $b);
+    }
+
+    if ($a == $b) {
+        return 0;
+    }
+
+    return ($a < $b) ? -1 : 1;
+}
+
+function print_usage($message = '')
+{
+    if (!empty($message)) {
+        echo "testsync.php: $message\n";
+    }
+
+    echo <<<USAGE
+Usage: testsync.php [OPTIONS]
+
+Possible options:
+  --url=RPCURL  Use curl to simulate access to URL for rpc.php. If not
+                specified, rpc.php is called internally.
+  --dir=DIR     Run test with data in directory DIR. If not spefied use
+                all directories starting with testcase_.
+  --setup       Don not run any tests. Just create test user syncmltest with
+                clean database. This does the setup before recording a test
+                case.
+  --debug       Produce some debug output.
+
+USAGE;
+    exit;
+}
diff --git a/framework/Text_Diff/Diff.php b/framework/Text_Diff/Diff.php
new file mode 100644 (file)
index 0000000..353d2ac
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+/**
+ * General API for generating and formatting diffs - the differences between
+ * two sequences of strings.
+ *
+ * The original PHP version of this code was written by Geoffrey T. Dairiki
+ * <dairiki@dairiki.org>, and is used/adapted with his permission.
+ *
+ * $Horde: framework/Text_Diff/Diff.php,v 1.29 2009/07/14 00:25:31 mrubinsk Exp $
+ *
+ * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ */
+class Text_Diff {
+
+    /**
+     * Array of changes.
+     *
+     * @var array
+     */
+    var $_edits;
+
+    /**
+     * Computes diffs between sequences of strings.
+     *
+     * @param string $engine     Name of the diffing engine to use.  'auto'
+     *                           will automatically select the best.
+     * @param array $params      Parameters to pass to the diffing engine.
+     *                           Normally an array of two arrays, each
+     *                           containing the lines from a file.
+     */
+    function Text_Diff($engine, $params)
+    {
+        // Backward compatibility workaround.
+        if (!is_string($engine)) {
+            $params = array($engine, $params);
+            $engine = 'auto';
+        }
+
+        if ($engine == 'auto') {
+            $engine = extension_loaded('xdiff') ? 'xdiff' : 'native';
+        } else {
+            $engine = basename($engine);
+        }
+
+        require_once 'Text/Diff/Engine/' . $engine . '.php';
+        $class = 'Text_Diff_Engine_' . $engine;
+        $diff_engine = new $class();
+
+        $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params);
+    }
+
+    /**
+     * Returns the array of differences.
+     */
+    function getDiff()
+    {
+        return $this->_edits;
+    }
+
+    /**
+     * returns the number of new (added) lines in a given diff.
+     *
+     * @since Text_Diff 1.1.0
+     * @since Horde 3.2
+     *
+     * @return integer The number of new lines
+     */
+    function countAddedLines()
+    {
+        $count = 0;
+        foreach ($this->_edits as $edit) {
+            if (is_a($edit, 'Text_Diff_Op_add') ||
+                is_a($edit, 'Text_Diff_Op_change')) {
+                $count += $edit->nfinal();
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Returns the number of deleted (removed) lines in a given diff.
+     *
+     * @since Text_Diff 1.1.0
+     * @since Horde 3.2
+     *
+     * @return integer The number of deleted lines
+     */
+    function countDeletedLines()
+    {
+        $count = 0;
+        foreach ($this->_edits as $edit) {
+            if (is_a($edit, 'Text_Diff_Op_delete') ||
+                is_a($edit, 'Text_Diff_Op_change')) {
+                $count += $edit->norig();
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Computes a reversed diff.
+     *
+     * Example:
+     * <code>
+     * $diff = new Text_Diff($lines1, $lines2);
+     * $rev = $diff->reverse();
+     * </code>
+     *
+     * @return Text_Diff  A Diff object representing the inverse of the
+     *                    original diff.  Note that we purposely don't return a
+     *                    reference here, since this essentially is a clone()
+     *                    method.
+     */
+    function reverse()
+    {
+        if (version_compare(zend_version(), '2', '>')) {
+            $rev = clone($this);
+        } else {
+            $rev = $this;
+        }
+        $rev->_edits = array();
+        foreach ($this->_edits as $edit) {
+            $rev->_edits[] = $edit->reverse();
+        }
+        return $rev;
+    }
+
+    /**
+     * Checks for an empty diff.
+     *
+     * @return boolean  True if two sequences were identical.
+     */
+    function isEmpty()
+    {
+        foreach ($this->_edits as $edit) {
+            if (!is_a($edit, 'Text_Diff_Op_copy')) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Computes the length of the Longest Common Subsequence (LCS).
+     *
+     * This is mostly for diagnostic purposes.
+     *
+     * @return integer  The length of the LCS.
+     */
+    function lcs()
+    {
+        $lcs = 0;
+        foreach ($this->_edits as $edit) {
+            if (is_a($edit, 'Text_Diff_Op_copy')) {
+                $lcs += count($edit->orig);
+            }
+        }
+        return $lcs;
+    }
+
+    /**
+     * Gets the original set of lines.
+     *
+     * This reconstructs the $from_lines parameter passed to the constructor.
+     *
+     * @return array  The original sequence of strings.
+     */
+    function getOriginal()
+    {
+        $lines = array();
+        foreach ($this->_edits as $edit) {
+            if ($edit->orig) {
+                array_splice($lines, count($lines), 0, $edit->orig);
+            }
+        }
+        return $lines;
+    }
+
+    /**
+     * Gets the final set of lines.
+     *
+     * This reconstructs the $to_lines parameter passed to the constructor.
+     *
+     * @return array  The sequence of strings.
+     */
+    function getFinal()
+    {
+        $lines = array();
+        foreach ($this->_edits as $edit) {
+            if ($edit->final) {
+                array_splice($lines, count($lines), 0, $edit->final);
+            }
+        }
+        return $lines;
+    }
+
+    /**
+     * Removes trailing newlines from a line of text. This is meant to be used
+     * with array_walk().
+     *
+     * @param string $line  The line to trim.
+     * @param integer $key  The index of the line in the array. Not used.
+     */
+    function trimNewlines(&$line, $key)
+    {
+        $line = str_replace(array("\n", "\r"), '', $line);
+    }
+
+    /**
+     * Determines the location of the system temporary directory.
+     *
+     * @static
+     *
+     * @access protected
+     *
+     * @return string  A directory name which can be used for temp files.
+     *                 Returns false if one could not be found.
+     */
+    function _getTempDir()
+    {
+        $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp',
+                               'c:\windows\temp', 'c:\winnt\temp');
+
+        /* Try PHP's upload_tmp_dir directive. */
+        $tmp = ini_get('upload_tmp_dir');
+
+        /* Otherwise, try to determine the TMPDIR environment variable. */
+        if (!strlen($tmp)) {
+            $tmp = getenv('TMPDIR');
+        }
+
+        /* If we still cannot determine a value, then cycle through a list of
+         * preset possibilities. */
+        while (!strlen($tmp) && count($tmp_locations)) {
+            $tmp_check = array_shift($tmp_locations);
+            if (@is_dir($tmp_check)) {
+                $tmp = $tmp_check;
+            }
+        }
+
+        /* If it is still empty, we have failed, so return false; otherwise
+         * return the directory determined. */
+        return strlen($tmp) ? $tmp : false;
+    }
+
+    /**
+     * Checks a diff for validity.
+     *
+     * This is here only for debugging purposes.
+     */
+    function _check($from_lines, $to_lines)
+    {
+        if (serialize($from_lines) != serialize($this->getOriginal())) {
+            trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+        }
+        if (serialize($to_lines) != serialize($this->getFinal())) {
+            trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
+        }
+
+        $rev = $this->reverse();
+        if (serialize($to_lines) != serialize($rev->getOriginal())) {
+            trigger_error("Reversed original doesn't match", E_USER_ERROR);
+        }
+        if (serialize($from_lines) != serialize($rev->getFinal())) {
+            trigger_error("Reversed final doesn't match", E_USER_ERROR);
+        }
+
+        $prevtype = null;
+        foreach ($this->_edits as $edit) {
+            if ($prevtype == get_class($edit)) {
+                trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+            }
+            $prevtype = get_class($edit);
+        }
+
+        return true;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ */
+class Text_MappedDiff extends Text_Diff {
+
+    /**
+     * Computes a diff between sequences of strings.
+     *
+     * This can be used to compute things like case-insensitve diffs, or diffs
+     * which ignore changes in white-space.
+     *
+     * @param array $from_lines         An array of strings.
+     * @param array $to_lines           An array of strings.
+     * @param array $mapped_from_lines  This array should have the same size
+     *                                  number of elements as $from_lines.  The
+     *                                  elements in $mapped_from_lines and
+     *                                  $mapped_to_lines are what is actually
+     *                                  compared when computing the diff.
+     * @param array $mapped_to_lines    This array should have the same number
+     *                                  of elements as $to_lines.
+     */
+    function Text_MappedDiff($from_lines, $to_lines,
+                             $mapped_from_lines, $mapped_to_lines)
+    {
+        assert(count($from_lines) == count($mapped_from_lines));
+        assert(count($to_lines) == count($mapped_to_lines));
+
+        parent::Text_Diff($mapped_from_lines, $mapped_to_lines);
+
+        $xi = $yi = 0;
+        for ($i = 0; $i < count($this->_edits); $i++) {
+            $orig = &$this->_edits[$i]->orig;
+            if (is_array($orig)) {
+                $orig = array_slice($from_lines, $xi, count($orig));
+                $xi += count($orig);
+            }
+
+            $final = &$this->_edits[$i]->final;
+            if (is_array($final)) {
+                $final = array_slice($to_lines, $yi, count($final));
+                $yi += count($final);
+            }
+        }
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_Op {
+
+    var $orig;
+    var $final;
+
+    function &reverse()
+    {
+        trigger_error('Abstract method', E_USER_ERROR);
+    }
+
+    function norig()
+    {
+        return $this->orig ? count($this->orig) : 0;
+    }
+
+    function nfinal()
+    {
+        return $this->final ? count($this->final) : 0;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_Op_copy extends Text_Diff_Op {
+
+    function Text_Diff_Op_copy($orig, $final = false)
+    {
+        if (!is_array($final)) {
+            $final = $orig;
+        }
+        $this->orig = $orig;
+        $this->final = $final;
+    }
+
+    function &reverse()
+    {
+        $reverse = new Text_Diff_Op_copy($this->final, $this->orig);
+        return $reverse;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_Op_delete extends Text_Diff_Op {
+
+    function Text_Diff_Op_delete($lines)
+    {
+        $this->orig = $lines;
+        $this->final = false;
+    }
+
+    function &reverse()
+    {
+        $reverse = new Text_Diff_Op_add($this->orig);
+        return $reverse;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_Op_add extends Text_Diff_Op {
+
+    function Text_Diff_Op_add($lines)
+    {
+        $this->final = $lines;
+        $this->orig = false;
+    }
+
+    function &reverse()
+    {
+        $reverse = new Text_Diff_Op_delete($this->final);
+        return $reverse;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_Op_change extends Text_Diff_Op {
+
+    function Text_Diff_Op_change($orig, $final)
+    {
+        $this->orig = $orig;
+        $this->final = $final;
+    }
+
+    function &reverse()
+    {
+        $reverse = new Text_Diff_Op_change($this->final, $this->orig);
+        return $reverse;
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Engine/native.php b/framework/Text_Diff/Diff/Engine/native.php
new file mode 100644 (file)
index 0000000..dd4008e
--- /dev/null
@@ -0,0 +1,438 @@
+<?php
+/**
+ * Class used internally by Text_Diff to actually compute the diffs.
+ *
+ * This class is implemented using native PHP code.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas (and a bit of code) are taken from analyze.c, of GNU
+ * diffutils-2.7, which can be found at:
+ * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
+ * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
+ * code was written by him, and is used/adapted with his permission.
+ *
+ * $Horde: framework/Text_Diff/Diff/Engine/native.php,v 1.13 2009/07/14 00:25:31 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * @package Text_Diff
+ */
+class Text_Diff_Engine_native {
+
+    function diff($from_lines, $to_lines)
+    {
+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
+
+        $n_from = count($from_lines);
+        $n_to = count($to_lines);
+
+        $this->xchanged = $this->ychanged = array();
+        $this->xv = $this->yv = array();
+        $this->xind = $this->yind = array();
+        unset($this->seq);
+        unset($this->in_seq);
+        unset($this->lcs);
+
+        // Skip leading common lines.
+        for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+            if ($from_lines[$skip] !== $to_lines[$skip]) {
+                break;
+            }
+            $this->xchanged[$skip] = $this->ychanged[$skip] = false;
+        }
+
+        // Skip trailing common lines.
+        $xi = $n_from; $yi = $n_to;
+        for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+            if ($from_lines[$xi] !== $to_lines[$yi]) {
+                break;
+            }
+            $this->xchanged[$xi] = $this->ychanged[$yi] = false;
+        }
+
+        // Ignore lines which do not exist in both files.
+        for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+            $xhash[$from_lines[$xi]] = 1;
+        }
+        for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+            $line = $to_lines[$yi];
+            if (($this->ychanged[$yi] = empty($xhash[$line]))) {
+                continue;
+            }
+            $yhash[$line] = 1;
+            $this->yv[] = $line;
+            $this->yind[] = $yi;
+        }
+        for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+            $line = $from_lines[$xi];
+            if (($this->xchanged[$xi] = empty($yhash[$line]))) {
+                continue;
+            }
+            $this->xv[] = $line;
+            $this->xind[] = $xi;
+        }
+
+        // Find the LCS.
+        $this->_compareseq(0, count($this->xv), 0, count($this->yv));
+
+        // Merge edits when possible.
+        $this->_shiftBoundaries($from_lines, $this->xchanged, $this->ychanged);
+        $this->_shiftBoundaries($to_lines, $this->ychanged, $this->xchanged);
+
+        // Compute the edit operations.
+        $edits = array();
+        $xi = $yi = 0;
+        while ($xi < $n_from || $yi < $n_to) {
+            assert($yi < $n_to || $this->xchanged[$xi]);
+            assert($xi < $n_from || $this->ychanged[$yi]);
+
+            // Skip matching "snake".
+            $copy = array();
+            while ($xi < $n_from && $yi < $n_to
+                   && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+                $copy[] = $from_lines[$xi++];
+                ++$yi;
+            }
+            if ($copy) {
+                $edits[] = new Text_Diff_Op_copy($copy);
+            }
+
+            // Find deletes & adds.
+            $delete = array();
+            while ($xi < $n_from && $this->xchanged[$xi]) {
+                $delete[] = $from_lines[$xi++];
+            }
+
+            $add = array();
+            while ($yi < $n_to && $this->ychanged[$yi]) {
+                $add[] = $to_lines[$yi++];
+            }
+
+            if ($delete && $add) {
+                $edits[] = new Text_Diff_Op_change($delete, $add);
+            } elseif ($delete) {
+                $edits[] = new Text_Diff_Op_delete($delete);
+            } elseif ($add) {
+                $edits[] = new Text_Diff_Op_add($add);
+            }
+        }
+
+        return $edits;
+    }
+
+    /**
+     * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
+     * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized
+     * segments.
+     *
+     * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an array of
+     * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
+     * sequences.  The first sub-sequence is contained in (X0, X1), (Y0, Y1),
+     * the second in (X1, X2), (Y1, Y2) and so on.  Note that (X0, Y0) ==
+     * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+     *
+     * This function assumes that the first lines of the specified portions of
+     * the two files do not match, and likewise that the last lines do not
+     * match.  The caller must trim matching lines from the beginning and end
+     * of the portions it is going to specify.
+     */
+    function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks)
+    {
+        $flip = false;
+
+        if ($xlim - $xoff > $ylim - $yoff) {
+            /* Things seems faster (I'm not sure I understand why) when the
+             * shortest sequence is in X. */
+            $flip = true;
+            list ($xoff, $xlim, $yoff, $ylim)
+                = array($yoff, $ylim, $xoff, $xlim);
+        }
+
+        if ($flip) {
+            for ($i = $ylim - 1; $i >= $yoff; $i--) {
+                $ymatches[$this->xv[$i]][] = $i;
+            }
+        } else {
+            for ($i = $ylim - 1; $i >= $yoff; $i--) {
+                $ymatches[$this->yv[$i]][] = $i;
+            }
+        }
+
+        $this->lcs = 0;
+        $this->seq[0]= $yoff - 1;
+        $this->in_seq = array();
+        $ymids[0] = array();
+
+        $numer = $xlim - $xoff + $nchunks - 1;
+        $x = $xoff;
+        for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+            if ($chunk > 0) {
+                for ($i = 0; $i <= $this->lcs; $i++) {
+                    $ymids[$i][$chunk - 1] = $this->seq[$i];
+                }
+            }
+
+            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks);
+            for (; $x < $x1; $x++) {
+                $line = $flip ? $this->yv[$x] : $this->xv[$x];
+                if (empty($ymatches[$line])) {
+                    continue;
+                }
+                $matches = $ymatches[$line];
+                reset($matches);
+                while (list(, $y) = each($matches)) {
+                    if (empty($this->in_seq[$y])) {
+                        $k = $this->_lcsPos($y);
+                        assert($k > 0);
+                        $ymids[$k] = $ymids[$k - 1];
+                        break;
+                    }
+                }
+                while (list(, $y) = each($matches)) {
+                    if ($y > $this->seq[$k - 1]) {
+                        assert($y <= $this->seq[$k]);
+                        /* Optimization: this is a common case: next match is
+                         * just replacing previous match. */
+                        $this->in_seq[$this->seq[$k]] = false;
+                        $this->seq[$k] = $y;
+                        $this->in_seq[$y] = 1;
+                    } elseif (empty($this->in_seq[$y])) {
+                        $k = $this->_lcsPos($y);
+                        assert($k > 0);
+                        $ymids[$k] = $ymids[$k - 1];
+                    }
+                }
+            }
+        }
+
+        $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+        $ymid = $ymids[$this->lcs];
+        for ($n = 0; $n < $nchunks - 1; $n++) {
+            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+            $y1 = $ymid[$n] + 1;
+            $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+        }
+        $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+        return array($this->lcs, $seps);
+    }
+
+    function _lcsPos($ypos)
+    {
+        $end = $this->lcs;
+        if ($end == 0 || $ypos > $this->seq[$end]) {
+            $this->seq[++$this->lcs] = $ypos;
+            $this->in_seq[$ypos] = 1;
+            return $this->lcs;
+        }
+
+        $beg = 1;
+        while ($beg < $end) {
+            $mid = (int)(($beg + $end) / 2);
+            if ($ypos > $this->seq[$mid]) {
+                $beg = $mid + 1;
+            } else {
+                $end = $mid;
+            }
+        }
+
+        assert($ypos != $this->seq[$end]);
+
+        $this->in_seq[$this->seq[$end]] = false;
+        $this->seq[$end] = $ypos;
+        $this->in_seq[$ypos] = 1;
+        return $end;
+    }
+
+    /**
+     * Finds LCS of two sequences.
+     *
+     * The results are recorded in the vectors $this->{x,y}changed[], by
+     * storing a 1 in the element for each line that is an insertion or
+     * deletion (ie. is not in the LCS).
+     *
+     * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
+     *
+     * Note that XLIM, YLIM are exclusive bounds.  All line numbers are
+     * origin-0 and discarded lines are not counted.
+     */
+    function _compareseq ($xoff, $xlim, $yoff, $ylim)
+    {
+        /* Slide down the bottom initial diagonal. */
+        while ($xoff < $xlim && $yoff < $ylim
+               && $this->xv[$xoff] == $this->yv[$yoff]) {
+            ++$xoff;
+            ++$yoff;
+        }
+
+        /* Slide up the top initial diagonal. */
+        while ($xlim > $xoff && $ylim > $yoff
+               && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+            --$xlim;
+            --$ylim;
+        }
+
+        if ($xoff == $xlim || $yoff == $ylim) {
+            $lcs = 0;
+        } else {
+            /* This is ad hoc but seems to work well.  $nchunks =
+             * sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); $nchunks =
+             * max(2,min(8,(int)$nchunks)); */
+            $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+            list($lcs, $seps)
+                = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+        }
+
+        if ($lcs == 0) {
+            /* X and Y sequences have no common subsequence: mark all
+             * changed. */
+            while ($yoff < $ylim) {
+                $this->ychanged[$this->yind[$yoff++]] = 1;
+            }
+            while ($xoff < $xlim) {
+                $this->xchanged[$this->xind[$xoff++]] = 1;
+            }
+        } else {
+            /* Use the partitions to split this problem into subproblems. */
+            reset($seps);
+            $pt1 = $seps[0];
+            while ($pt2 = next($seps)) {
+                $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+                $pt1 = $pt2;
+            }
+        }
+    }
+
+    /**
+     * Adjusts inserts/deletes of identical lines to join changes as much as
+     * possible.
+     *
+     * We do something when a run of changed lines include a line at one end
+     * and has an excluded, identical line at the other.  We are free to
+     * choose which identical line is included.  `compareseq' usually chooses
+     * the one at the beginning, but usually it is cleaner to consider the
+     * following identical line to be the "change".
+     *
+     * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+     */
+    function _shiftBoundaries($lines, &$changed, $other_changed)
+    {
+        $i = 0;
+        $j = 0;
+
+        assert('count($lines) == count($changed)');
+        $len = count($lines);
+        $other_len = count($other_changed);
+
+        while (1) {
+            /* Scan forward to find the beginning of another run of
+             * changes. Also keep track of the corresponding point in the
+             * other file.
+             *
+             * Throughout this code, $i and $j are adjusted together so that
+             * the first $i elements of $changed and the first $j elements of
+             * $other_changed both contain the same number of zeros (unchanged
+             * lines).
+             *
+             * Furthermore, $j is always kept so that $j == $other_len or
+             * $other_changed[$j] == false. */
+            while ($j < $other_len && $other_changed[$j]) {
+                $j++;
+            }
+
+            while ($i < $len && ! $changed[$i]) {
+                assert('$j < $other_len && ! $other_changed[$j]');
+                $i++; $j++;
+                while ($j < $other_len && $other_changed[$j]) {
+                    $j++;
+                }
+            }
+
+            if ($i == $len) {
+                break;
+            }
+
+            $start = $i;
+
+            /* Find the end of this run of changes. */
+            while (++$i < $len && $changed[$i]) {
+                continue;
+            }
+
+            do {
+                /* Record the length of this run of changes, so that we can
+                 * later determine whether the run has grown. */
+                $runlength = $i - $start;
+
+                /* Move the changed region back, so long as the previous
+                 * unchanged line matches the last changed one.  This merges
+                 * with previous changed regions. */
+                while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+                    $changed[--$start] = 1;
+                    $changed[--$i] = false;
+                    while ($start > 0 && $changed[$start - 1]) {
+                        $start--;
+                    }
+                    assert('$j > 0');
+                    while ($other_changed[--$j]) {
+                        continue;
+                    }
+                    assert('$j >= 0 && !$other_changed[$j]');
+                }
+
+                /* Set CORRESPONDING to the end of the changed run, at the
+                 * last point where it corresponds to a changed run in the
+                 * other file. CORRESPONDING == LEN means no such point has
+                 * been found. */
+                $corresponding = $j < $other_len ? $i : $len;
+
+                /* Move the changed region forward, so long as the first
+                 * changed line matches the following unchanged one.  This
+                 * merges with following changed regions.  Do this second, so
+                 * that if there are no merges, the changed region is moved
+                 * forward as far as possible. */
+                while ($i < $len && $lines[$start] == $lines[$i]) {
+                    $changed[$start++] = false;
+                    $changed[$i++] = 1;
+                    while ($i < $len && $changed[$i]) {
+                        $i++;
+                    }
+
+                    assert('$j < $other_len && ! $other_changed[$j]');
+                    $j++;
+                    if ($j < $other_len && $other_changed[$j]) {
+                        $corresponding = $i;
+                        while ($j < $other_len && $other_changed[$j]) {
+                            $j++;
+                        }
+                    }
+                }
+            } while ($runlength != $i - $start);
+
+            /* If possible, move the fully-merged run of changes back to a
+             * corresponding run in the other file. */
+            while ($corresponding < $i) {
+                $changed[--$start] = 1;
+                $changed[--$i] = 0;
+                assert('$j > 0');
+                while ($other_changed[--$j]) {
+                    continue;
+                }
+                assert('$j >= 0 && !$other_changed[$j]');
+            }
+        }
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Engine/shell.php b/framework/Text_Diff/Diff/Engine/shell.php
new file mode 100644 (file)
index 0000000..1686ca4
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * This class uses the Unix `diff` program via shell_exec to compute the
+ * differences between the two input arrays.
+ *
+ * $Horde: framework/Text_Diff/Diff/Engine/shell.php,v 1.9 2009/01/06 17:49:52 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Milian Wolff <mail@milianw.de>
+ * @package Text_Diff
+ * @since   0.3.0
+ */
+class Text_Diff_Engine_shell {
+
+    /**
+     * Path to the diff executable
+     *
+     * @var string
+     */
+    var $_diffCommand = 'diff';
+
+    /**
+     * Returns the array of differences.
+     *
+     * @param array $from_lines lines of text from old file
+     * @param array $to_lines   lines of text from new file
+     *
+     * @return array all changes made (array with Text_Diff_Op_* objects)
+     */
+    function diff($from_lines, $to_lines)
+    {
+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
+
+        $temp_dir = Text_Diff::_getTempDir();
+
+        // Execute gnu diff or similar to get a standard diff file.
+        $from_file = tempnam($temp_dir, 'Text_Diff');
+        $to_file = tempnam($temp_dir, 'Text_Diff');
+        $fp = fopen($from_file, 'w');
+        fwrite($fp, implode("\n", $from_lines));
+        fclose($fp);
+        $fp = fopen($to_file, 'w');
+        fwrite($fp, implode("\n", $to_lines));
+        fclose($fp);
+        $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file);
+        unlink($from_file);
+        unlink($to_file);
+
+        if (is_null($diff)) {
+            // No changes were made
+            return array(new Text_Diff_Op_copy($from_lines));
+        }
+
+        $from_line_no = 1;
+        $to_line_no = 1;
+        $edits = array();
+
+        // Get changed lines by parsing something like:
+        // 0a1,2
+        // 1,2c4,6
+        // 1,5d6
+        preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff,
+            $matches, PREG_SET_ORDER);
+
+        foreach ($matches as $match) {
+            if (!isset($match[5])) {
+                // This paren is not set every time (see regex).
+                $match[5] = false;
+            }
+
+            if ($match[3] == 'a') {
+                $from_line_no--;
+            }
+
+            if ($match[3] == 'd') {
+                $to_line_no--;
+            }
+
+            if ($from_line_no < $match[1] || $to_line_no < $match[4]) {
+                // copied lines
+                assert('$match[1] - $from_line_no == $match[4] - $to_line_no');
+                array_push($edits,
+                    new Text_Diff_Op_copy(
+                        $this->_getLines($from_lines, $from_line_no, $match[1] - 1),
+                        $this->_getLines($to_lines, $to_line_no, $match[4] - 1)));
+            }
+
+            switch ($match[3]) {
+            case 'd':
+                // deleted lines
+                array_push($edits,
+                    new Text_Diff_Op_delete(
+                        $this->_getLines($from_lines, $from_line_no, $match[2])));
+                $to_line_no++;
+                break;
+
+            case 'c':
+                // changed lines
+                array_push($edits,
+                    new Text_Diff_Op_change(
+                        $this->_getLines($from_lines, $from_line_no, $match[2]),
+                        $this->_getLines($to_lines, $to_line_no, $match[5])));
+                break;
+
+            case 'a':
+                // added lines
+                array_push($edits,
+                    new Text_Diff_Op_add(
+                        $this->_getLines($to_lines, $to_line_no, $match[5])));
+                $from_line_no++;
+                break;
+            }
+        }
+
+        if (!empty($from_lines)) {
+            // Some lines might still be pending. Add them as copied
+            array_push($edits,
+                new Text_Diff_Op_copy(
+                    $this->_getLines($from_lines, $from_line_no,
+                                     $from_line_no + count($from_lines) - 1),
+                    $this->_getLines($to_lines, $to_line_no,
+                                     $to_line_no + count($to_lines) - 1)));
+        }
+
+        return $edits;
+    }
+
+    /**
+     * Get lines from either the old or new text
+     *
+     * @access private
+     *
+     * @param array &$text_lines Either $from_lines or $to_lines
+     * @param int   &$line_no    Current line number
+     * @param int   $end         Optional end line, when we want to chop more
+     *                           than one line.
+     *
+     * @return array The chopped lines
+     */
+    function _getLines(&$text_lines, &$line_no, $end = false)
+    {
+        if (!empty($end)) {
+            $lines = array();
+            // We can shift even more
+            while ($line_no <= $end) {
+                array_push($lines, array_shift($text_lines));
+                $line_no++;
+            }
+        } else {
+            $lines = array(array_shift($text_lines));
+            $line_no++;
+        }
+
+        return $lines;
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Engine/string.php b/framework/Text_Diff/Diff/Engine/string.php
new file mode 100644 (file)
index 0000000..524fab1
--- /dev/null
@@ -0,0 +1,250 @@
+<?php
+/**
+ * Parses unified or context diffs output from eg. the diff utility.
+ *
+ * Example:
+ * <code>
+ * $patch = file_get_contents('example.patch');
+ * $diff = new Text_Diff('string', array($patch));
+ * $renderer = new Text_Diff_Renderer_inline();
+ * echo $renderer->render($diff);
+ * </code>
+ *
+ * $Horde: framework/Text_Diff/Diff/Engine/string.php,v 1.11 2009/07/24 12:26:30 jan Exp $
+ *
+ * Copyright 2005 Ã–rjan Persson <o@42mm.org>
+ * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Ã–rjan Persson <o@42mm.org>
+ * @package Text_Diff
+ * @since   0.2.0
+ */
+class Text_Diff_Engine_string {
+
+    /**
+     * Parses a unified or context diff.
+     *
+     * First param contains the whole diff and the second can be used to force
+     * a specific diff type. If the second parameter is 'autodetect', the
+     * diff will be examined to find out which type of diff this is.
+     *
+     * @param string $diff  The diff content.
+     * @param string $mode  The diff mode of the content in $diff. One of
+     *                      'context', 'unified', or 'autodetect'.
+     *
+     * @return array  List of all diff operations.
+     */
+    function diff($diff, $mode = 'autodetect')
+    {
+        // Detect line breaks.
+        $lnbr = "\n";
+        if (strpos($diff, "\r\n") !== false) {
+            $lnbr = "\r\n";
+        } elseif (strpos($diff, "\r") !== false) {
+            $lnbr = "\r";
+        }
+
+        // Make sure we have a line break at the EOF.
+        if (substr($diff, -strlen($lnbr)) != $lnbr) {
+            $diff .= $lnbr;
+        }
+
+        if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') {
+            return PEAR::raiseError('Type of diff is unsupported');
+        }
+
+        if ($mode == 'autodetect') {
+            $context = strpos($diff, '***');
+            $unified = strpos($diff, '---');
+            if ($context === $unified) {
+                return PEAR::raiseError('Type of diff could not be detected');
+            } elseif ($context === false || $unified === false) {
+                $mode = $context !== false ? 'context' : 'unified';
+            } else {
+                $mode = $context < $unified ? 'context' : 'unified';
+            }
+        }
+
+        // Split by new line and remove the diff header, if there is one.
+        $diff = explode($lnbr, $diff);
+        if (($mode == 'context' && strpos($diff[0], '***') === 0) ||
+            ($mode == 'unified' && strpos($diff[0], '---') === 0)) {
+            array_shift($diff);
+            array_shift($diff);
+        }
+
+        if ($mode == 'context') {
+            return $this->parseContextDiff($diff);
+        } else {
+            return $this->parseUnifiedDiff($diff);
+        }
+    }
+
+    /**
+     * Parses an array containing the unified diff.
+     *
+     * @param array $diff  Array of lines.
+     *
+     * @return array  List of all diff operations.
+     */
+    function parseUnifiedDiff($diff)
+    {
+        $edits = array();
+        $end = count($diff) - 1;
+        for ($i = 0; $i < $end;) {
+            $diff1 = array();
+            switch (substr($diff[$i], 0, 1)) {
+            case ' ':
+                do {
+                    $diff1[] = substr($diff[$i], 1);
+                } while (++$i < $end && substr($diff[$i], 0, 1) == ' ');
+                $edits[] = new Text_Diff_Op_copy($diff1);
+                break;
+
+            case '+':
+                // get all new lines
+                do {
+                    $diff1[] = substr($diff[$i], 1);
+                } while (++$i < $end && substr($diff[$i], 0, 1) == '+');
+                $edits[] = new Text_Diff_Op_add($diff1);
+                break;
+
+            case '-':
+                // get changed or removed lines
+                $diff2 = array();
+                do {
+                    $diff1[] = substr($diff[$i], 1);
+                } while (++$i < $end && substr($diff[$i], 0, 1) == '-');
+
+                while ($i < $end && substr($diff[$i], 0, 1) == '+') {
+                    $diff2[] = substr($diff[$i++], 1);
+                }
+                if (count($diff2) == 0) {
+                    $edits[] = new Text_Diff_Op_delete($diff1);
+                } else {
+                    $edits[] = new Text_Diff_Op_change($diff1, $diff2);
+                }
+                break;
+
+            default:
+                $i++;
+                break;
+            }
+        }
+
+        return $edits;
+    }
+
+    /**
+     * Parses an array containing the context diff.
+     *
+     * @param array $diff  Array of lines.
+     *
+     * @return array  List of all diff operations.
+     */
+    function parseContextDiff(&$diff)
+    {
+        $edits = array();
+        $i = $max_i = $j = $max_j = 0;
+        $end = count($diff) - 1;
+        while ($i < $end && $j < $end) {
+            while ($i >= $max_i && $j >= $max_j) {
+                // Find the boundaries of the diff output of the two files
+                for ($i = $j;
+                     $i < $end && substr($diff[$i], 0, 3) == '***';
+                     $i++);
+                for ($max_i = $i;
+                     $max_i < $end && substr($diff[$max_i], 0, 3) != '---';
+                     $max_i++);
+                for ($j = $max_i;
+                     $j < $end && substr($diff[$j], 0, 3) == '---';
+                     $j++);
+                for ($max_j = $j;
+                     $max_j < $end && substr($diff[$max_j], 0, 3) != '***';
+                     $max_j++);
+            }
+
+            // find what hasn't been changed
+            $array = array();
+            while ($i < $max_i &&
+                   $j < $max_j &&
+                   strcmp($diff[$i], $diff[$j]) == 0) {
+                $array[] = substr($diff[$i], 2);
+                $i++;
+                $j++;
+            }
+
+            while ($i < $max_i && ($max_j-$j) <= 1) {
+                if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') {
+                    break;
+                }
+                $array[] = substr($diff[$i++], 2);
+            }
+
+            while ($j < $max_j && ($max_i-$i) <= 1) {
+                if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') {
+                    break;
+                }
+                $array[] = substr($diff[$j++], 2);
+            }
+            if (count($array) > 0) {
+                $edits[] = new Text_Diff_Op_copy($array);
+            }
+
+            if ($i < $max_i) {
+                $diff1 = array();
+                switch (substr($diff[$i], 0, 1)) {
+                case '!':
+                    $diff2 = array();
+                    do {
+                        $diff1[] = substr($diff[$i], 2);
+                        if ($j < $max_j && substr($diff[$j], 0, 1) == '!') {
+                            $diff2[] = substr($diff[$j++], 2);
+                        }
+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!');
+                    $edits[] = new Text_Diff_Op_change($diff1, $diff2);
+                    break;
+
+                case '+':
+                    do {
+                        $diff1[] = substr($diff[$i], 2);
+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+');
+                    $edits[] = new Text_Diff_Op_add($diff1);
+                    break;
+
+                case '-':
+                    do {
+                        $diff1[] = substr($diff[$i], 2);
+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-');
+                    $edits[] = new Text_Diff_Op_delete($diff1);
+                    break;
+                }
+            }
+
+            if ($j < $max_j) {
+                $diff2 = array();
+                switch (substr($diff[$j], 0, 1)) {
+                case '+':
+                    do {
+                        $diff2[] = substr($diff[$j++], 2);
+                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '+');
+                    $edits[] = new Text_Diff_Op_add($diff2);
+                    break;
+
+                case '-':
+                    do {
+                        $diff2[] = substr($diff[$j++], 2);
+                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '-');
+                    $edits[] = new Text_Diff_Op_delete($diff2);
+                    break;
+                }
+            }
+        }
+
+        return $edits;
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Engine/xdiff.php b/framework/Text_Diff/Diff/Engine/xdiff.php
new file mode 100644 (file)
index 0000000..bcc4e85
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * This class uses the xdiff PECL package (http://pecl.php.net/package/xdiff)
+ * to compute the differences between the two input arrays.
+ *
+ * $Horde: framework/Text_Diff/Diff/Engine/xdiff.php,v 1.9 2009/07/24 13:02:00 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Jon Parise <jon@horde.org>
+ * @package Text_Diff
+ */
+class Text_Diff_Engine_xdiff {
+
+    /**
+     */
+    function diff($from_lines, $to_lines)
+    {
+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
+
+        /* Convert the two input arrays into strings for xdiff processing. */
+        $from_string = implode("\n", $from_lines);
+        $to_string = implode("\n", $to_lines);
+
+        /* Diff the two strings and convert the result to an array. */
+        $diff = xdiff_string_diff($from_string, $to_string, count($to_lines));
+        $diff = explode("\n", $diff);
+
+        /* Walk through the diff one line at a time.  We build the $edits
+         * array of diff operations by reading the first character of the
+         * xdiff output (which is in the "unified diff" format).
+         *
+         * Note that we don't have enough information to detect "changed"
+         * lines using this approach, so we can't add Text_Diff_Op_changed
+         * instances to the $edits array.  The result is still perfectly
+         * valid, albeit a little less descriptive and efficient. */
+        $edits = array();
+        foreach ($diff as $line) {
+            if (!strlen($line)) {
+                continue;
+            }
+            switch ($line[0]) {
+            case ' ':
+                $edits[] = new Text_Diff_Op_copy(array(substr($line, 1)));
+                break;
+
+            case '+':
+                $edits[] = new Text_Diff_Op_add(array(substr($line, 1)));
+                break;
+
+            case '-':
+                $edits[] = new Text_Diff_Op_delete(array(substr($line, 1)));
+                break;
+            }
+        }
+
+        return $edits;
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Mapped.php b/framework/Text_Diff/Diff/Mapped.php
new file mode 100644 (file)
index 0000000..0eb2c6f
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * $Horde: framework/Text_Diff/Diff/Mapped.php,v 1.6 2009/01/06 17:49:52 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ */
+class Text_Diff_Mapped extends Text_Diff {
+
+    /**
+     * Computes a diff between sequences of strings.
+     *
+     * This can be used to compute things like case-insensitve diffs, or diffs
+     * which ignore changes in white-space.
+     *
+     * @param array $from_lines         An array of strings.
+     * @param array $to_lines           An array of strings.
+     * @param array $mapped_from_lines  This array should have the same size
+     *                                  number of elements as $from_lines.  The
+     *                                  elements in $mapped_from_lines and
+     *                                  $mapped_to_lines are what is actually
+     *                                  compared when computing the diff.
+     * @param array $mapped_to_lines    This array should have the same number
+     *                                  of elements as $to_lines.
+     */
+    function Text_Diff_Mapped($from_lines, $to_lines,
+                              $mapped_from_lines, $mapped_to_lines)
+    {
+        assert(count($from_lines) == count($mapped_from_lines));
+        assert(count($to_lines) == count($mapped_to_lines));
+
+        parent::Text_Diff($mapped_from_lines, $mapped_to_lines);
+
+        $xi = $yi = 0;
+        for ($i = 0; $i < count($this->_edits); $i++) {
+            $orig = &$this->_edits[$i]->orig;
+            if (is_array($orig)) {
+                $orig = array_slice($from_lines, $xi, count($orig));
+                $xi += count($orig);
+            }
+
+            $final = &$this->_edits[$i]->final;
+            if (is_array($final)) {
+                $final = array_slice($to_lines, $yi, count($final));
+                $yi += count($final);
+            }
+        }
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Renderer.php b/framework/Text_Diff/Diff/Renderer.php
new file mode 100644 (file)
index 0000000..c07e6b1
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+/**
+ * A class to render Diffs in different formats.
+ *
+ * This class renders the diff in classic diff format. It is intended that
+ * this class be customized via inheritance, to obtain fancier outputs.
+ *
+ * $Horde: framework/Text_Diff/Diff/Renderer.php,v 1.23 2009/07/14 00:25:31 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @package Text_Diff
+ */
+class Text_Diff_Renderer {
+
+    /**
+     * Number of leading context "lines" to preserve.
+     *
+     * This should be left at zero for this class, but subclasses may want to
+     * set this to other values.
+     */
+    var $_leading_context_lines = 0;
+
+    /**
+     * Number of trailing context "lines" to preserve.
+     *
+     * This should be left at zero for this class, but subclasses may want to
+     * set this to other values.
+     */
+    var $_trailing_context_lines = 0;
+
+    /**
+     * Constructor.
+     */
+    function Text_Diff_Renderer($params = array())
+    {
+        foreach ($params as $param => $value) {
+            $v = '_' . $param;
+            if (isset($this->$v)) {
+                $this->$v = $value;
+            }
+        }
+    }
+
+    /**
+     * Get any renderer parameters.
+     *
+     * @return array  All parameters of this renderer object.
+     */
+    function getParams()
+    {
+        $params = array();
+        foreach (get_object_vars($this) as $k => $v) {
+            if ($k[0] == '_') {
+                $params[substr($k, 1)] = $v;
+            }
+        }
+
+        return $params;
+    }
+
+    /**
+     * Renders a diff.
+     *
+     * @param Text_Diff $diff  A Text_Diff object.
+     *
+     * @return string  The formatted output.
+     */
+    function render($diff)
+    {
+        $xi = $yi = 1;
+        $block = false;
+        $context = array();
+
+        $nlead = $this->_leading_context_lines;
+        $ntrail = $this->_trailing_context_lines;
+
+        $output = $this->_startDiff();
+
+        $diffs = $diff->getDiff();
+        foreach ($diffs as $i => $edit) {
+            /* If these are unchanged (copied) lines, and we want to keep
+             * leading or trailing context lines, extract them from the copy
+             * block. */
+            if (is_a($edit, 'Text_Diff_Op_copy')) {
+                /* Do we have any diff blocks yet? */
+                if (is_array($block)) {
+                    /* How many lines to keep as context from the copy
+                     * block. */
+                    $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail;
+                    if (count($edit->orig) <= $keep) {
+                        /* We have less lines in the block than we want for
+                         * context => keep the whole block. */
+                        $block[] = $edit;
+                    } else {
+                        if ($ntrail) {
+                            /* Create a new block with as many lines as we need
+                             * for the trailing context. */
+                            $context = array_slice($edit->orig, 0, $ntrail);
+                            $block[] = new Text_Diff_Op_copy($context);
+                        }
+                        /* @todo */
+                        $output .= $this->_block($x0, $ntrail + $xi - $x0,
+                                                 $y0, $ntrail + $yi - $y0,
+                                                 $block);
+                        $block = false;
+                    }
+                }
+                /* Keep the copy block as the context for the next block. */
+                $context = $edit->orig;
+            } else {
+                /* Don't we have any diff blocks yet? */
+                if (!is_array($block)) {
+                    /* Extract context lines from the preceding copy block. */
+                    $context = array_slice($context, count($context) - $nlead);
+                    $x0 = $xi - count($context);
+                    $y0 = $yi - count($context);
+                    $block = array();
+                    if ($context) {
+                        $block[] = new Text_Diff_Op_copy($context);
+                    }
+                }
+                $block[] = $edit;
+            }
+
+            if ($edit->orig) {
+                $xi += count($edit->orig);
+            }
+            if ($edit->final) {
+                $yi += count($edit->final);
+            }
+        }
+
+        if (is_array($block)) {
+            $output .= $this->_block($x0, $xi - $x0,
+                                     $y0, $yi - $y0,
+                                     $block);
+        }
+
+        return $output . $this->_endDiff();
+    }
+
+    function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
+    {
+        $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen));
+
+        foreach ($edits as $edit) {
+            switch (strtolower(get_class($edit))) {
+            case 'text_diff_op_copy':
+                $output .= $this->_context($edit->orig);
+                break;
+
+            case 'text_diff_op_add':
+                $output .= $this->_added($edit->final);
+                break;
+
+            case 'text_diff_op_delete':
+                $output .= $this->_deleted($edit->orig);
+                break;
+
+            case 'text_diff_op_change':
+                $output .= $this->_changed($edit->orig, $edit->final);
+                break;
+            }
+        }
+
+        return $output . $this->_endBlock();
+    }
+
+    function _startDiff()
+    {
+        return '';
+    }
+
+    function _endDiff()
+    {
+        return '';
+    }
+
+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
+    {
+        if ($xlen > 1) {
+            $xbeg .= ',' . ($xbeg + $xlen - 1);
+        }
+        if ($ylen > 1) {
+            $ybeg .= ',' . ($ybeg + $ylen - 1);
+        }
+
+        // this matches the GNU Diff behaviour
+        if ($xlen && !$ylen) {
+            $ybeg--;
+        } elseif (!$xlen) {
+            $xbeg--;
+        }
+
+        return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+    }
+
+    function _startBlock($header)
+    {
+        return $header . "\n";
+    }
+
+    function _endBlock()
+    {
+        return '';
+    }
+
+    function _lines($lines, $prefix = ' ')
+    {
+        return $prefix . implode("\n$prefix", $lines) . "\n";
+    }
+
+    function _context($lines)
+    {
+        return $this->_lines($lines, '  ');
+    }
+
+    function _added($lines)
+    {
+        return $this->_lines($lines, '> ');
+    }
+
+    function _deleted($lines)
+    {
+        return $this->_lines($lines, '< ');
+    }
+
+    function _changed($orig, $final)
+    {
+        return $this->_deleted($orig) . "---\n" . $this->_added($final);
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Renderer/context.php b/framework/Text_Diff/Diff/Renderer/context.php
new file mode 100644 (file)
index 0000000..4440756
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * "Context" diff renderer.
+ *
+ * This class renders the diff in classic "context diff" format.
+ *
+ * $Horde: framework/Text_Diff/Diff/Renderer/context.php,v 1.6 2009/01/06 17:49:52 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @package Text_Diff
+ */
+
+/** Text_Diff_Renderer */
+require_once 'Text/Diff/Renderer.php';
+
+/**
+ * @package Text_Diff
+ */
+class Text_Diff_Renderer_context extends Text_Diff_Renderer {
+
+    /**
+     * Number of leading context "lines" to preserve.
+     */
+    var $_leading_context_lines = 4;
+
+    /**
+     * Number of trailing context "lines" to preserve.
+     */
+    var $_trailing_context_lines = 4;
+
+    var $_second_block = '';
+
+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
+    {
+        if ($xlen != 1) {
+            $xbeg .= ',' . $xlen;
+        }
+        if ($ylen != 1) {
+            $ybeg .= ',' . $ylen;
+        }
+        $this->_second_block = "--- $ybeg ----\n";
+        return "***************\n*** $xbeg ****";
+    }
+
+    function _endBlock()
+    {
+        return $this->_second_block;
+    }
+
+    function _context($lines)
+    {
+        $this->_second_block .= $this->_lines($lines, '  ');
+        return $this->_lines($lines, '  ');
+    }
+
+    function _added($lines)
+    {
+        $this->_second_block .= $this->_lines($lines, '+ ');
+        return '';
+    }
+
+    function _deleted($lines)
+    {
+        return $this->_lines($lines, '- ');
+    }
+
+    function _changed($orig, $final)
+    {
+        $this->_second_block .= $this->_lines($final, '! ');
+        return $this->_lines($orig, '! ');
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Renderer/inline.php b/framework/Text_Diff/Diff/Renderer/inline.php
new file mode 100644 (file)
index 0000000..2314fea
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+/**
+ * "Inline" diff renderer.
+ *
+ * $Horde: framework/Text_Diff/Diff/Renderer/inline.php,v 1.24 2009/09/03 10:14:21 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Ciprian Popovici
+ * @package Text_Diff
+ */
+
+/** Text_Diff_Renderer */
+require_once 'Text/Diff/Renderer.php';
+
+/**
+ * "Inline" diff renderer.
+ *
+ * This class renders diffs in the Wiki-style "inline" format.
+ *
+ * @author  Ciprian Popovici
+ * @package Text_Diff
+ */
+class Text_Diff_Renderer_inline extends Text_Diff_Renderer {
+
+    /**
+     * Number of leading context "lines" to preserve.
+     *
+     * @var integer
+     */
+    var $_leading_context_lines = 10000;
+
+    /**
+     * Number of trailing context "lines" to preserve.
+     *
+     * @var integer
+     */
+    var $_trailing_context_lines = 10000;
+
+    /**
+     * Prefix for inserted text.
+     *
+     * @var string
+     */
+    var $_ins_prefix = '<ins>';
+
+    /**
+     * Suffix for inserted text.
+     *
+     * @var string
+     */
+    var $_ins_suffix = '</ins>';
+
+    /**
+     * Prefix for deleted text.
+     *
+     * @var string
+     */
+    var $_del_prefix = '<del>';
+
+    /**
+     * Suffix for deleted text.
+     *
+     * @var string
+     */
+    var $_del_suffix = '</del>';
+
+    /**
+     * Header for each change block.
+     *
+     * @var string
+     */
+    var $_block_header = '';
+
+    /**
+     * Whether to split down to character-level.
+     *
+     * @var boolean
+     */
+    var $_split_characters = false;
+
+    /**
+     * What are we currently splitting on? Used to recurse to show word-level
+     * or character-level changes.
+     *
+     * @var string
+     */
+    var $_split_level = 'lines';
+
+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
+    {
+        return $this->_block_header;
+    }
+
+    function _startBlock($header)
+    {
+        return $header;
+    }
+
+    function _lines($lines, $prefix = ' ', $encode = true)
+    {
+        if ($encode) {
+            array_walk($lines, array(&$this, '_encode'));
+        }
+
+        if ($this->_split_level == 'lines') {
+            return implode("\n", $lines) . "\n";
+        } else {
+            return implode('', $lines);
+        }
+    }
+
+    function _added($lines)
+    {
+        array_walk($lines, array(&$this, '_encode'));
+        $lines[0] = $this->_ins_prefix . $lines[0];
+        $lines[count($lines) - 1] .= $this->_ins_suffix;
+        return $this->_lines($lines, ' ', false);
+    }
+
+    function _deleted($lines, $words = false)
+    {
+        array_walk($lines, array(&$this, '_encode'));
+        $lines[0] = $this->_del_prefix . $lines[0];
+        $lines[count($lines) - 1] .= $this->_del_suffix;
+        return $this->_lines($lines, ' ', false);
+    }
+
+    function _changed($orig, $final)
+    {
+        /* If we've already split on characters, just display. */
+        if ($this->_split_level == 'characters') {
+            return $this->_deleted($orig)
+                . $this->_added($final);
+        }
+
+        /* If we've already split on words, just display. */
+        if ($this->_split_level == 'words') {
+            $prefix = '';
+            while ($orig[0] !== false && $final[0] !== false &&
+                   substr($orig[0], 0, 1) == ' ' &&
+                   substr($final[0], 0, 1) == ' ') {
+                $prefix .= substr($orig[0], 0, 1);
+                $orig[0] = substr($orig[0], 1);
+                $final[0] = substr($final[0], 1);
+            }
+            return $prefix . $this->_deleted($orig) . $this->_added($final);
+        }
+
+        $text1 = implode("\n", $orig);
+        $text2 = implode("\n", $final);
+
+        /* Non-printing newline marker. */
+        $nl = "\0";
+
+        if ($this->_split_characters) {
+            $diff = new Text_Diff('native',
+                                  array(preg_split('//', $text1),
+                                        preg_split('//', $text2)));
+        } else {
+            /* We want to split on word boundaries, but we need to preserve
+             * whitespace as well. Therefore we split on words, but include
+             * all blocks of whitespace in the wordlist. */
+            $diff = new Text_Diff('native',
+                                  array($this->_splitOnWords($text1, $nl),
+                                        $this->_splitOnWords($text2, $nl)));
+        }
+
+        /* Get the diff in inline format. */
+        $renderer = new Text_Diff_Renderer_inline
+            (array_merge($this->getParams(),
+                         array('split_level' => $this->_split_characters ? 'characters' : 'words')));
+
+        /* Run the diff and get the output. */
+        return str_replace($nl, "\n", $renderer->render($diff)) . "\n";
+    }
+
+    function _splitOnWords($string, $newlineEscape = "\n")
+    {
+        // Ignore \0; otherwise the while loop will never finish.
+        $string = str_replace("\0", '', $string);
+
+        $words = array();
+        $length = strlen($string);
+        $pos = 0;
+
+        while ($pos < $length) {
+            // Eat a word with any preceding whitespace.
+            $spaces = strspn(substr($string, $pos), " \n");
+            $nextpos = strcspn(substr($string, $pos + $spaces), " \n");
+            $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos));
+            $pos += $spaces + $nextpos;
+        }
+
+        return $words;
+    }
+
+    function _encode(&$string)
+    {
+        $string = htmlspecialchars($string);
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/Renderer/unified.php b/framework/Text_Diff/Diff/Renderer/unified.php
new file mode 100644 (file)
index 0000000..b1fccf5
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * "Unified" diff renderer.
+ *
+ * This class renders the diff in classic "unified diff" format.
+ *
+ * $Horde: framework/Text_Diff/Diff/Renderer/unified.php,v 1.11 2009/01/06 17:49:52 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @author  Ciprian Popovici
+ * @package Text_Diff
+ */
+
+/** Text_Diff_Renderer */
+require_once 'Text/Diff/Renderer.php';
+
+/**
+ * @package Text_Diff
+ */
+class Text_Diff_Renderer_unified extends Text_Diff_Renderer {
+
+    /**
+     * Number of leading context "lines" to preserve.
+     */
+    var $_leading_context_lines = 4;
+
+    /**
+     * Number of trailing context "lines" to preserve.
+     */
+    var $_trailing_context_lines = 4;
+
+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
+    {
+        if ($xlen != 1) {
+            $xbeg .= ',' . $xlen;
+        }
+        if ($ylen != 1) {
+            $ybeg .= ',' . $ylen;
+        }
+        return "@@ -$xbeg +$ybeg @@";
+    }
+
+    function _context($lines)
+    {
+        return $this->_lines($lines, ' ');
+    }
+
+    function _added($lines)
+    {
+        return $this->_lines($lines, '+');
+    }
+
+    function _deleted($lines)
+    {
+        return $this->_lines($lines, '-');
+    }
+
+    function _changed($orig, $final)
+    {
+        return $this->_deleted($orig) . $this->_added($final);
+    }
+
+}
diff --git a/framework/Text_Diff/Diff/ThreeWay.php b/framework/Text_Diff/Diff/ThreeWay.php
new file mode 100644 (file)
index 0000000..8d21357
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+/**
+ * A class for computing three way diffs.
+ *
+ * $Horde: framework/Text_Diff/Diff/ThreeWay.php,v 1.6 2009/01/06 17:49:52 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you did
+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
+ *
+ * @package Text_Diff
+ * @since   0.3.0
+ */
+
+/** Text_Diff */
+require_once 'Text/Diff.php';
+
+/**
+ * A class for computing three way diffs.
+ *
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ */
+class Text_Diff_ThreeWay extends Text_Diff {
+
+    /**
+     * Conflict counter.
+     *
+     * @var integer
+     */
+    var $_conflictingBlocks = 0;
+
+    /**
+     * Computes diff between 3 sequences of strings.
+     *
+     * @param array $orig    The original lines to use.
+     * @param array $final1  The first version to compare to.
+     * @param array $final2  The second version to compare to.
+     */
+    function Text_Diff_ThreeWay($orig, $final1, $final2)
+    {
+        if (extension_loaded('xdiff')) {
+            $engine = new Text_Diff_Engine_xdiff();
+        } else {
+            $engine = new Text_Diff_Engine_native();
+        }
+
+        $this->_edits = $this->_diff3($engine->diff($orig, $final1),
+                                      $engine->diff($orig, $final2));
+    }
+
+    /**
+     */
+    function mergedOutput($label1 = false, $label2 = false)
+    {
+        $lines = array();
+        foreach ($this->_edits as $edit) {
+            if ($edit->isConflict()) {
+                /* FIXME: this should probably be moved somewhere else. */
+                $lines = array_merge($lines,
+                                     array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')),
+                                     $edit->final1,
+                                     array("======="),
+                                     $edit->final2,
+                                     array('>>>>>>>' . ($label2 ? ' ' . $label2 : '')));
+                $this->_conflictingBlocks++;
+            } else {
+                $lines = array_merge($lines, $edit->merged());
+            }
+        }
+
+        return $lines;
+    }
+
+    /**
+     * @access private
+     */
+    function _diff3($edits1, $edits2)
+    {
+        $edits = array();
+        $bb = new Text_Diff_ThreeWay_BlockBuilder();
+
+        $e1 = current($edits1);
+        $e2 = current($edits2);
+        while ($e1 || $e2) {
+            if ($e1 && $e2 && is_a($e1, 'Text_Diff_Op_copy') && is_a($e2, 'Text_Diff_Op_copy')) {
+                /* We have copy blocks from both diffs. This is the (only)
+                 * time we want to emit a diff3 copy block.  Flush current
+                 * diff3 diff block, if any. */
+                if ($edit = $bb->finish()) {
+                    $edits[] = $edit;
+                }
+
+                $ncopy = min($e1->norig(), $e2->norig());
+                assert($ncopy > 0);
+                $edits[] = new Text_Diff_ThreeWay_Op_copy(array_slice($e1->orig, 0, $ncopy));
+
+                if ($e1->norig() > $ncopy) {
+                    array_splice($e1->orig, 0, $ncopy);
+                    array_splice($e1->final, 0, $ncopy);
+                } else {
+                    $e1 = next($edits1);
+                }
+
+                if ($e2->norig() > $ncopy) {
+                    array_splice($e2->orig, 0, $ncopy);
+                    array_splice($e2->final, 0, $ncopy);
+                } else {
+                    $e2 = next($edits2);
+                }
+            } else {
+                if ($e1 && $e2) {
+                    if ($e1->orig && $e2->orig) {
+                        $norig = min($e1->norig(), $e2->norig());
+                        $orig = array_splice($e1->orig, 0, $norig);
+                        array_splice($e2->orig, 0, $norig);
+                        $bb->input($orig);
+                    }
+
+                    if (is_a($e1, 'Text_Diff_Op_copy')) {
+                        $bb->out1(array_splice($e1->final, 0, $norig));
+                    }
+
+                    if (is_a($e2, 'Text_Diff_Op_copy')) {
+                        $bb->out2(array_splice($e2->final, 0, $norig));
+                    }
+                }
+
+                if ($e1 && ! $e1->orig) {
+                    $bb->out1($e1->final);
+                    $e1 = next($edits1);
+                }
+                if ($e2 && ! $e2->orig) {
+                    $bb->out2($e2->final);
+                    $e2 = next($edits2);
+                }
+            }
+        }
+
+        if ($edit = $bb->finish()) {
+            $edits[] = $edit;
+        }
+
+        return $edits;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_ThreeWay_Op {
+
+    function Text_Diff_ThreeWay_Op($orig = false, $final1 = false, $final2 = false)
+    {
+        $this->orig = $orig ? $orig : array();
+        $this->final1 = $final1 ? $final1 : array();
+        $this->final2 = $final2 ? $final2 : array();
+    }
+
+    function merged()
+    {
+        if (!isset($this->_merged)) {
+            if ($this->final1 === $this->final2) {
+                $this->_merged = &$this->final1;
+            } elseif ($this->final1 === $this->orig) {
+                $this->_merged = &$this->final2;
+            } elseif ($this->final2 === $this->orig) {
+                $this->_merged = &$this->final1;
+            } else {
+                $this->_merged = false;
+            }
+        }
+
+        return $this->_merged;
+    }
+
+    function isConflict()
+    {
+        return $this->merged() === false;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_ThreeWay_Op_copy extends Text_Diff_ThreeWay_Op {
+
+    function Text_Diff_ThreeWay_Op_Copy($lines = false)
+    {
+        $this->orig = $lines ? $lines : array();
+        $this->final1 = &$this->orig;
+        $this->final2 = &$this->orig;
+    }
+
+    function merged()
+    {
+        return $this->orig;
+    }
+
+    function isConflict()
+    {
+        return false;
+    }
+
+}
+
+/**
+ * @package Text_Diff
+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class Text_Diff_ThreeWay_BlockBuilder {
+
+    function Text_Diff_ThreeWay_BlockBuilder()
+    {
+        $this->_init();
+    }
+
+    function input($lines)
+    {
+        if ($lines) {
+            $this->_append($this->orig, $lines);
+        }
+    }
+
+    function out1($lines)
+    {
+        if ($lines) {
+            $this->_append($this->final1, $lines);
+        }
+    }
+
+    function out2($lines)
+    {
+        if ($lines) {
+            $this->_append($this->final2, $lines);
+        }
+    }
+
+    function isEmpty()
+    {
+        return !$this->orig && !$this->final1 && !$this->final2;
+    }
+
+    function finish()
+    {
+        if ($this->isEmpty()) {
+            return false;
+        } else {
+            $edit = new Text_Diff_ThreeWay_Op($this->orig, $this->final1, $this->final2);
+            $this->_init();
+            return $edit;
+        }
+    }
+
+    function _init()
+    {
+        $this->orig = $this->final1 = $this->final2 = array();
+    }
+
+    function _append(&$array, $lines)
+    {
+        array_splice($array, sizeof($array), 0, $lines);
+    }
+
+}
diff --git a/framework/Text_Diff/docs/examples/1.txt b/framework/Text_Diff/docs/examples/1.txt
new file mode 100644 (file)
index 0000000..976c447
--- /dev/null
@@ -0,0 +1,3 @@
+This line is the same.
+This line is different in 1.txt
+This line is the same.
diff --git a/framework/Text_Diff/docs/examples/2.txt b/framework/Text_Diff/docs/examples/2.txt
new file mode 100644 (file)
index 0000000..6fa8051
--- /dev/null
@@ -0,0 +1,3 @@
+This line is the same.
+This line is different in 2.txt
+This line is the same.
diff --git a/framework/Text_Diff/docs/examples/diff.php b/framework/Text_Diff/docs/examples/diff.php
new file mode 100644 (file)
index 0000000..66914c2
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/php
+<?php
+/**
+ * Text_Diff example script.
+ *
+ * Take two files from the command line args and produce a unified
+ * diff of them.
+ *
+ * @package Text_Diff
+ */
+
+require_once 'Text/Diff.php';
+require_once 'Text/Diff/Renderer.php';
+require_once 'Text/Diff/Renderer/unified.php';
+
+/* Make sure we have enough arguments. */
+if (count($argv) < 3) {
+    echo "Usage: diff.php <file1> <file2>\n\n";
+    exit;
+}
+
+/* Make sure both files exist. */
+if (!is_readable($argv[1])) {
+    echo "$argv[1] not found or not readable.\n\n";
+}
+if (!is_readable($argv[2])) {
+    echo "$argv[2] not found or not readable.\n\n";
+}
+
+/* Load the lines of each file. */
+$lines1 = file($argv[1]);
+$lines2 = file($argv[2]);
+
+/* Create the Diff object. */
+$diff = new Text_Diff('auto', array($lines1, $lines2));
+
+/* Output the diff in unified format. */
+$renderer = new Text_Diff_Renderer_unified();
+echo $renderer->render($diff);
diff --git a/framework/Text_Diff/package.xml b/framework/Text_Diff/package.xml
new file mode 100644 (file)
index 0000000..ad3eb3e
--- /dev/null
@@ -0,0 +1,300 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Text_Diff</name>
+ <channel>pear.horde.org</channel>
+ <summary>Engine for performing and rendering text diffs</summary>
+ <description>This package provides a text-based diff engine and renderers for multiple diff output formats.</description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-09-10</date>
+ <version>
+  <release>1.1.0</release>
+  <api>1.1.0</api>
+ </version>
+ <stability>
+  <release>stable</release>
+  <api>stable</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+ <notes>* Add countAddedLines() and countDeletedLines() methods (Christian King, PEAR Request #13183).
+* Improve string engine if patch data has no header (PEAR Bug #14533).
+* Fix autodetection of diff type in string engine (PEAR Bug #14625).</notes>
+ <contents>
+  <dir name="/">
+   <dir name="Diff">
+    <dir name="Engine">
+     <file baseinstalldir="/Text" name="native.php" role="php" />
+     <file baseinstalldir="/Text" name="shell.php" role="php" />
+     <file baseinstalldir="/Text" name="string.php" role="php" />
+     <file baseinstalldir="/Text" name="xdiff.php" role="php" />
+    </dir> <!-- /Diff/Engine -->
+    <dir name="Renderer">
+     <file baseinstalldir="/Text" name="context.php" role="php" />
+     <file baseinstalldir="/Text" name="inline.php" role="php" />
+     <file baseinstalldir="/Text" name="unified.php" role="php" />
+    </dir> <!-- /Diff/Renderer -->
+    <file baseinstalldir="/Text" name="Mapped.php" role="php" />
+    <file baseinstalldir="/Text" name="Renderer.php" role="php" />
+    <file baseinstalldir="/Text" name="ThreeWay.php" role="php" />
+   </dir> <!-- /Diff -->
+   <dir name="docs">
+    <dir name="examples">
+     <file baseinstalldir="/Text" name="1.txt" role="doc" />
+     <file baseinstalldir="/Text" name="2.txt" role="doc" />
+     <file baseinstalldir="/Text" name="diff.php" role="doc" />
+    </dir> <!-- /docs/examples -->
+   </dir> <!-- /docs -->
+   <dir name="tests">
+    <file baseinstalldir="/Text" name="1.txt" role="test" />
+    <file baseinstalldir="/Text" name="2.txt" role="test" />
+    <file baseinstalldir="/Text" name="3.txt" role="test" />
+    <file baseinstalldir="/Text" name="4.txt" role="test" />
+    <file baseinstalldir="/Text" name="5.txt" role="test" />
+    <file baseinstalldir="/Text" name="6.txt" role="test" />
+    <file baseinstalldir="/Text" name="context.patch" role="test" />
+    <file baseinstalldir="/Text" name="context.phpt" role="test" />
+    <file baseinstalldir="/Text" name="context2.phpt" role="test" />
+    <file baseinstalldir="/Text" name="diff.phpt" role="test" />
+    <file baseinstalldir="/Text" name="inline.phpt" role="test" />
+    <file baseinstalldir="/Text" name="inline2.phpt" role="test" />
+    <file baseinstalldir="/Text" name="pear_bug12740.phpt" role="test" />
+    <file baseinstalldir="/Text" name="pear_bug4879.phpt" role="test" />
+    <file baseinstalldir="/Text" name="pear_bug6251.phpt" role="test" />
+    <file baseinstalldir="/Text" name="pear_bug6428.phpt" role="test" />
+    <file baseinstalldir="/Text" name="pear_bug7839.phpt" role="test" />
+    <file baseinstalldir="/Text" name="string.phpt" role="test" />
+    <file baseinstalldir="/Text" name="unified.patch" role="test" />
+    <file baseinstalldir="/Text" name="unified.phpt" role="test" />
+    <file baseinstalldir="/Text" name="unified2.patch" role="test" />
+    <file baseinstalldir="/Text" name="unified2.phpt" role="test" />
+    <file baseinstalldir="/Text" name="xdiff.phpt" role="test" />
+   </dir> <!-- /tests -->
+   <file baseinstalldir="/Text" name="Diff.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+  </required>
+  <optional>
+   <extension>
+    <name>xdiff</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+    <version>
+     <release>1.0.0</release>
+     <api>1.0.0</api>
+    </version>
+    <stability>
+     <release>stable</release>
+     <api>stable</api>
+    </stability>
+    <date>2008-01-31</date>
+    <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+    <notes>* Stable release; final non-bugfix PHP 4 release.</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.3.2</release>
+    <api>0.3.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-01-04</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fixed assertion in the native engine with PHP 5 (PEAR Bug #12740).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.3.1</release>
+    <api>0.3.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-10-28</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fixed off-by-one errors with line numbers in default renderer output (Milian Wolff, PEAR Bugs #7839, #12111).
+* Restore compatibility with PHP 4 in the shell diff engine.</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.3.0</release>
+    <api>0.3.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2007-09-20</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Converted to package.xml 2.0.
+* Added context diff renderer.
+* Stripped nul characters to avoid infinite loops (PEAR Bug #7921).
+* Fix assertion with PHP 5 (PEAR Bug #10423).
+* Add shell diff engine (Milian Wolff, PEAR Request #12051).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.2.1</release>
+    <api>0.2.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-02-06</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fix Text_Diff::reverse() method with PHP 5 (PEAR Bug #6699).
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.2.0</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-01-06</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fix reference warnings with PHP 4.4 and PHP 5.1.
+* Fix too many trailing context lines for the last diff chunk. (PEAR Bug #6251).
+* Add diff engine for existing diffs/patches (Örjan Persson, PEAR Request #5373).
+* Fix problems with numbers, whitespace-only changes and the inline renderer (PEAR Bug #6428.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.1</release>
+    <api>0.1.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-08-19</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Restore compatibility with PHP 4.2.0 (PEAR Bug #5152).
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-08-16</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fix Renderer::getParams() (PEAR Bug #4549)
+* Encode HTML entities in the inline renderer (Horde Bug #2181)
+* Fix endless loop in inline renderer with certain whitespaces (PEAR Bug #4879)
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.5</release>
+    <api>0.0.5</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-05-04</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Fixed variable name in Text_MappedDiff.
+* Removed output buffering code.
+* Fixed additional empty lines being added with the inline renderer (Bug 3730).
+* Fixed multiple calls to _endBlock per block (Bug 4065).
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.4</release>
+    <api>0.0.4</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-10-13</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>* Generalized Diff Renderer parameter handling.
+* Added an inline renderer contributed by Ciprian Popovici (ciprian@zuavra.net).
+* Fixed PHPDoc.
+* Fixed a PHP 5 incompatibility.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-05-13</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>Renamed final() and orig() methods to avoid conflict with final keyword in PHP5
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-03-16</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>Added optional xdiff support
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-09</date>
+   <license uri="http://opensource.org/licenses/lgpl-license.php">LGPL</license>
+   <notes>Initial package release
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Text_Diff/tests/1.txt b/framework/Text_Diff/tests/1.txt
new file mode 100644 (file)
index 0000000..976c447
--- /dev/null
@@ -0,0 +1,3 @@
+This line is the same.
+This line is different in 1.txt
+This line is the same.
diff --git a/framework/Text_Diff/tests/2.txt b/framework/Text_Diff/tests/2.txt
new file mode 100644 (file)
index 0000000..6fa8051
--- /dev/null
@@ -0,0 +1,3 @@
+This line is the same.
+This line is different in 2.txt
+This line is the same.
diff --git a/framework/Text_Diff/tests/3.txt b/framework/Text_Diff/tests/3.txt
new file mode 100644 (file)
index 0000000..0c26fe9
--- /dev/null
@@ -0,0 +1,4 @@
+This line is the same.
+This line is different in 1.txt
+This line is the same.
+This line is new in 3.txt
diff --git a/framework/Text_Diff/tests/4.txt b/framework/Text_Diff/tests/4.txt
new file mode 100644 (file)
index 0000000..7cbc69d
--- /dev/null
@@ -0,0 +1,3 @@
+This line is the same.
+This line is different in 4.txt
+This line is the same.
diff --git a/framework/Text_Diff/tests/5.txt b/framework/Text_Diff/tests/5.txt
new file mode 100644 (file)
index 0000000..d48db22
--- /dev/null
@@ -0,0 +1,5 @@
+This is a test.
+Adding random text to simulate files.
+Various Content.
+More Content.
+Testing diff and renderer.
diff --git a/framework/Text_Diff/tests/6.txt b/framework/Text_Diff/tests/6.txt
new file mode 100644 (file)
index 0000000..5b421cd
--- /dev/null
@@ -0,0 +1,7 @@
+This is a test.
+Adding random text to simulate files.
+Inserting a line.
+Various Content.
+Replacing content.
+Testing similarities and renderer.
+Append content.
diff --git a/framework/Text_Diff/tests/context.patch b/framework/Text_Diff/tests/context.patch
new file mode 100644 (file)
index 0000000..2a4fad9
--- /dev/null
@@ -0,0 +1,11 @@
+*** 1.txt      2005-03-21 13:37:59.000000000 +0100
+--- 2.txt      2005-03-21 13:38:00.000000000 +0100
+***************
+*** 1,3 ****
+  This line is the same.
+! This line is different in 1.txt
+  This line is the same.
+--- 1,3 ----
+  This line is the same.
+! This line is different in 2.txt
+  This line is the same.
diff --git a/framework/Text_Diff/tests/context.phpt b/framework/Text_Diff/tests/context.phpt
new file mode 100644 (file)
index 0000000..5891ea4
--- /dev/null
@@ -0,0 +1,25 @@
+--TEST--
+Text_Diff: Context renderer
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/context.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_context();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+***************
+*** 1,3 ****
+  This line is the same.
+! This line is different in 1.txt
+  This line is the same.
+--- 1,3 ----
+  This line is the same.
+! This line is different in 2.txt
+  This line is the same.
diff --git a/framework/Text_Diff/tests/context2.phpt b/framework/Text_Diff/tests/context2.phpt
new file mode 100644 (file)
index 0000000..6d8f248
--- /dev/null
@@ -0,0 +1,31 @@
+--TEST--
+Text_Diff: Context renderer 2
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/context.php';
+
+$lines1 = file(dirname(__FILE__) . '/5.txt');
+$lines2 = file(dirname(__FILE__) . '/6.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_context();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+***************
+*** 1,5 ****
+  This is a test.
+  Adding random text to simulate files.
+  Various Content.
+! More Content.
+! Testing diff and renderer.
+--- 1,7 ----
+  This is a test.
+  Adding random text to simulate files.
++ Inserting a line.
+  Various Content.
+! Replacing content.
+! Testing similarities and renderer.
+! Append content.
diff --git a/framework/Text_Diff/tests/diff.phpt b/framework/Text_Diff/tests/diff.phpt
new file mode 100644 (file)
index 0000000..add291f
--- /dev/null
@@ -0,0 +1,62 @@
+--TEST--
+Text_Diff: Basic diff operation
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+echo strtolower(print_r($diff, true));
+?>
+--EXPECT--
+text_diff object
+(
+    [_edits] => array
+        (
+            [0] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+            [1] => text_diff_op_change object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is different in 1.txt
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is different in 2.txt
+                        )
+
+                )
+
+            [2] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+        )
+
+)
diff --git a/framework/Text_Diff/tests/diff_shell.phpt b/framework/Text_Diff/tests/diff_shell.phpt
new file mode 100644 (file)
index 0000000..eb80c64
--- /dev/null
@@ -0,0 +1,62 @@
+--TEST--
+Text_Diff: Basic diff operation, shell engine
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+
+$diff = new Text_Diff('shell', array($lines1, $lines2));
+echo strtolower(print_r($diff, true));
+?>
+--EXPECT--
+text_diff object
+(
+    [_edits] => array
+        (
+            [0] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+            [1] => text_diff_op_change object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is different in 1.txt
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is different in 2.txt
+                        )
+
+                )
+
+            [2] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+        )
+
+)
diff --git a/framework/Text_Diff/tests/inline.phpt b/framework/Text_Diff/tests/inline.phpt
new file mode 100644 (file)
index 0000000..71b4538
--- /dev/null
@@ -0,0 +1,27 @@
+--TEST--
+Text_Diff: Inline renderer
+--FILE--
+<?php
+
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/inline.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_inline();
+echo $renderer->render($diff);
+
+$renderer = new Text_Diff_Renderer_inline(array('split_characters' => true));
+echo $renderer->render($diff);
+
+?>
+--EXPECT--
+This line is the same.
+This line is different in <del>1.txt</del><ins>2.txt</ins>
+This line is the same.
+This line is the same.
+This line is different in <del>1</del><ins>2</ins>.txt
+This line is the same.
diff --git a/framework/Text_Diff/tests/inline2.phpt b/framework/Text_Diff/tests/inline2.phpt
new file mode 100644 (file)
index 0000000..153a39f
--- /dev/null
@@ -0,0 +1,37 @@
+--TEST--
+Text_Diff: Inline renderer 2
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/inline.php';
+
+$lines1 = array(
+    "This is a test.\n",
+    "Adding random text to simulate files.\n",
+    "Various Content.\n",
+    "More Content.\n",
+    "Testing diff and renderer.\n"
+);
+$lines2 = array(
+    "This is a test.\n",
+    "Adding random text to simulate files.\n",
+    "Inserting a line.\n",
+    "Various Content.\n",
+    "Replacing content.\n",
+    "Testing similarities and renderer.\n",
+    "Append content.\n"
+);
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_inline();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+This is a test.
+Adding random text to simulate files.
+<ins>Inserting a line.</ins>
+Various Content.
+<del>More Content.</del><ins>Replacing content.</ins>
+Testing <del>diff</del><ins>similarities</ins> and renderer.<ins>
+Append content.</ins>
diff --git a/framework/Text_Diff/tests/pear_bug12740.phpt b/framework/Text_Diff/tests/pear_bug12740.phpt
new file mode 100644 (file)
index 0000000..56b4ced
--- /dev/null
@@ -0,0 +1,30 @@
+--TEST--
+Text_Diff: PEAR Bug #12740 (failed assertion)
+--FILE--
+<?php
+
+require dirname(__FILE__) . '/../Diff.php';
+require dirname(__FILE__) . '/../Diff/Renderer/inline.php';
+
+$a = <<<QQ
+<li>The tax credit amounts to 30% of the cost of the system, with a
+maximum of 2,000. This credit is separate from the 500 home improvement
+credit.</li>
+<h3>Fuel Cells<a
+href="12341234213421341234123412341234123421341234213412342134213423"
+class="anchor" title="Link to this section"><br />
+<li>Your fuel 123456789</li>
+QQ;
+
+$b = <<<QQ
+<li> of gas emissions by 2050</li>
+<li>Raise car fuel economy to 50 mpg by 2017</li>
+<li>Increase access to mass transit systems</li>
+QQ;
+
+$diff = new Text_Diff('native', array(explode("\n", $b), explode("\n", $a)));
+$renderer = new Text_Diff_Renderer_inline();
+$renderer->render($diff);
+
+?>
+--EXPECT--
diff --git a/framework/Text_Diff/tests/pear_bug4879.phpt b/framework/Text_Diff/tests/pear_bug4879.phpt
new file mode 100644 (file)
index 0000000..5bb657b
--- /dev/null
@@ -0,0 +1,30 @@
+--TEST--
+Text_Diff: PEAR Bug #4879 (inline renderer hangs on numbers in input string)
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/inline.php';
+
+$oldtext = <<<EOT
+Common text
+Bob had 1 apple, Alice had 2.
+Bon appetit!
+EOT;
+
+$newtext = <<< EOT
+Common text
+Bob had 10 apples, Alice had 1.
+Bon appetit!
+EOT;
+
+$oldpieces = explode ("\n", $oldtext);
+$newpieces = explode ("\n", $newtext);
+$diff = new Text_Diff('native', array($oldpieces, $newpieces));
+
+$renderer = new Text_Diff_Renderer_inline();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+Common text
+Bob had <del>1 apple,</del><ins>10 apples,</ins> Alice had <del>2.</del><ins>1.</ins>
+Bon appetit!
diff --git a/framework/Text_Diff/tests/pear_bug4982.phpt b/framework/Text_Diff/tests/pear_bug4982.phpt
new file mode 100644 (file)
index 0000000..7207f4a
--- /dev/null
@@ -0,0 +1,26 @@
+--TEST--
+Text_Diff: PEAR Bug #4982 (wrong line breaks with inline renderer)
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/inline.php';
+
+$oldtext = <<<EOT
+This line is different in 1.txt
+EOT;
+
+$newtext = <<<EOT
+This is new !!
+This line is different in 2.txt
+EOT;
+
+$oldpieces = explode("\n", $oldtext);
+$newpieces = explode("\n", $newtext);
+$diff = new Text_Diff('native', array($oldpieces, $newpieces));
+
+$renderer = new Text_Diff_Renderer_inline();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+<ins>This is new !!</ins>
+This line is different in <del>1.txt</del><ins>2.txt</ins>
diff --git a/framework/Text_Diff/tests/pear_bug6251.phpt b/framework/Text_Diff/tests/pear_bug6251.phpt
new file mode 100644 (file)
index 0000000..86af082
--- /dev/null
@@ -0,0 +1,45 @@
+--TEST--
+Text_Diff: PEAR Bug #6251 (too much trailing context)
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/unified.php';
+
+$oldtext = <<<EOT
+
+Original Text
+
+
+
+ss
+ttt
+EOT;
+
+$newtext = <<< EOT
+
+Modified Text
+
+
+
+ss
+ttt
+EOT;
+
+$oldpieces = explode ("\n", $oldtext);
+$newpieces = explode ("\n", $newtext);
+$diff = new Text_Diff('native', array($oldpieces, $newpieces));
+
+$renderer = new Text_Diff_Renderer_unified(array('leading_context_lines' => 3, 'trailing_context_lines' => 3));
+
+// We need to use var_dump, as the test runner strips trailing empty lines.
+var_dump($renderer->render($diff));
+?>
+--EXPECT--
+string(54) "@@ -1,5 +1,5 @@
+-Original Text
++Modified Text
+"
diff --git a/framework/Text_Diff/tests/pear_bug6428.phpt b/framework/Text_Diff/tests/pear_bug6428.phpt
new file mode 100644 (file)
index 0000000..9d5b982
--- /dev/null
@@ -0,0 +1,18 @@
+--TEST--
+Text_Diff: PEAR Bug #6428 (problem with single digits after space)
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/inline.php';
+
+$from = array('Line 1',  'Another line');
+$to   = array('Line  1', 'Another line');
+
+$diff = new Text_Diff('native', array($from, $to));
+$renderer = new Text_Diff_Renderer_inline();
+
+echo $renderer->render($diff);
+?>
+--EXPECT--
+Line <del>1</del><ins> 1</ins>
+Another line
diff --git a/framework/Text_Diff/tests/pear_bug7839.phpt b/framework/Text_Diff/tests/pear_bug7839.phpt
new file mode 100644 (file)
index 0000000..9073c15
--- /dev/null
@@ -0,0 +1,57 @@
+--TEST--
+Text_Diff: PEAR Bug #7839 ()
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer.php';
+
+$oldtext = <<<EOT
+This is line 1.
+This is line 2.
+This is line 3.
+This is line 4.
+This is line 5.
+This is line 6.
+This is line 7.
+This is line 8.
+This is line 9.
+EOT;
+
+$newtext = <<< EOT
+This is line 1.
+This was line 2.
+This is line 3.
+This is line 5.
+This was line 6.
+This was line 7.
+This was line 8.
+This is line 9.
+This is line 10.
+EOT;
+
+$oldpieces = explode ("\n", $oldtext);
+$newpieces = explode ("\n", $newtext);
+$diff = new Text_Diff('native', array($oldpieces, $newpieces));
+
+$renderer = new Text_Diff_Renderer();
+
+// We need to use var_dump, as the test runner strips trailing empty lines.
+echo($renderer->render($diff));
+?>
+--EXPECT--
+2c2
+< This is line 2.
+---
+> This was line 2.
+4d3
+< This is line 4.
+6,8c5,7
+< This is line 6.
+< This is line 7.
+< This is line 8.
+---
+> This was line 6.
+> This was line 7.
+> This was line 8.
+9a9
+> This is line 10.
diff --git a/framework/Text_Diff/tests/string.phpt b/framework/Text_Diff/tests/string.phpt
new file mode 100644 (file)
index 0000000..9001198
--- /dev/null
@@ -0,0 +1,145 @@
+--TEST--
+Text_Diff: Text_Diff_Engine_string test.
+--FILE--
+<?php
+
+require_once 'PEAR.php';
+require_once 'Text/Diff.php';
+
+$unified = file_get_contents(dirname(__FILE__) . '/unified.patch');
+$diff_u = new Text_Diff('string', array($unified));
+echo strtolower(print_r($diff_u, true));
+
+$unified2 = file_get_contents(dirname(__FILE__) . '/unified2.patch');
+$diff_u2 = new Text_Diff('string', array($unified2));
+var_export(is_a($diff_u2->getDiff(), 'PEAR_Error'));
+echo "\n";
+$diff_u2 = new Text_Diff('string', array($unified2, 'unified'));
+echo strtolower(print_r($diff_u2, true));
+
+$context = file_get_contents(dirname(__FILE__) . '/context.patch');
+$diff_c = new Text_Diff('string', array($context));
+echo strtolower(print_r($diff_c, true));
+
+?>
+--EXPECT--
+text_diff object
+(
+    [_edits] => array
+        (
+            [0] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+            [1] => text_diff_op_change object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is different in 1.txt
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is different in 2.txt
+                        )
+
+                )
+
+            [2] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+        )
+
+)
+true
+text_diff object
+(
+    [_edits] => array
+        (
+            [0] => text_diff_op_change object
+                (
+                    [orig] => array
+                        (
+                            [0] => for the first time in u.s. history number of private contractors and troops are equal
+                        )
+
+                    [final] => array
+                        (
+                            [0] => number of private contractors and troops are equal for first time in u.s. history
+                        )
+
+                )
+
+        )
+
+)
+text_diff object
+(
+    [_edits] => array
+        (
+            [0] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+            [1] => text_diff_op_change object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is different in 1.txt
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is different in 2.txt
+                        )
+
+                )
+
+            [2] => text_diff_op_copy object
+                (
+                    [orig] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                    [final] => array
+                        (
+                            [0] => this line is the same.
+                        )
+
+                )
+
+        )
+
+)
diff --git a/framework/Text_Diff/tests/unified.patch b/framework/Text_Diff/tests/unified.patch
new file mode 100644 (file)
index 0000000..b2b3f25
--- /dev/null
@@ -0,0 +1,7 @@
+--- 1.txt      2005-03-21 13:37:59.000000000 +0100
++++ 2.txt      2005-03-21 13:38:00.000000000 +0100
+@@ -1,3 +1,3 @@
+ This line is the same.
+-This line is different in 1.txt
++This line is different in 2.txt
+ This line is the same.
diff --git a/framework/Text_Diff/tests/unified.phpt b/framework/Text_Diff/tests/unified.phpt
new file mode 100644 (file)
index 0000000..4c279dd
--- /dev/null
@@ -0,0 +1,21 @@
+--TEST--
+Text_Diff: Unified renderer
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/unified.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_unified();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+@@ -1,3 +1,3 @@
+ This line is the same.
+-This line is different in 1.txt
++This line is different in 2.txt
+ This line is the same.
diff --git a/framework/Text_Diff/tests/unified2.patch b/framework/Text_Diff/tests/unified2.patch
new file mode 100644 (file)
index 0000000..2bee5db
--- /dev/null
@@ -0,0 +1,3 @@
+@@ -1 +1 @@
+-For the first time in U.S. history number of private contractors and troops are equal
++Number of private contractors and troops are equal for first time in U.S. history
diff --git a/framework/Text_Diff/tests/unified2.phpt b/framework/Text_Diff/tests/unified2.phpt
new file mode 100644 (file)
index 0000000..303b329
--- /dev/null
@@ -0,0 +1,26 @@
+--TEST--
+Text_Diff: Unified renderer 2
+--FILE--
+<?php
+include_once 'Text/Diff.php';
+include_once 'Text/Diff/Renderer/unified.php';
+
+$lines1 = file(dirname(__FILE__) . '/5.txt');
+$lines2 = file(dirname(__FILE__) . '/6.txt');
+
+$diff = new Text_Diff('native', array($lines1, $lines2));
+
+$renderer = new Text_Diff_Renderer_unified();
+echo $renderer->render($diff);
+?>
+--EXPECT--
+@@ -1,5 +1,7 @@
+ This is a test.
+ Adding random text to simulate files.
++Inserting a line.
+ Various Content.
+-More Content.
+-Testing diff and renderer.
++Replacing content.
++Testing similarities and renderer.
++Append content.
diff --git a/framework/Text_Diff/tests/xdiff.phpt b/framework/Text_Diff/tests/xdiff.phpt
new file mode 100644 (file)
index 0000000..0be5379
--- /dev/null
@@ -0,0 +1,24 @@
+--TEST--
+Text_Diff: Text_Diff_Engine_xdiff test.
+--SKIPIF--
+<?php if (!extension_loaded('xdiff')) echo 'skip xdiff extension not installed'; ?>
+--FILE--
+<?php
+
+require_once 'PEAR.php';
+require_once 'Text/Diff.php';
+require_once 'Text/Diff/Renderer/unified.php';
+
+$lines1 = file(dirname(__FILE__) . '/1.txt');
+$lines2 = file(dirname(__FILE__) . '/2.txt');
+$diff = new Text_Diff('xdiff', array($lines1, $lines2));
+$renderer = new Text_Diff_Renderer_unified();
+echo $renderer->render($diff);
+
+?>
+--EXPECT--
+@@ -1,3 +1,3 @@
+ This line is the same.
+-This line is different in 1.txt
++This line is different in 2.txt
+ This line is the same.
diff --git a/framework/VFS/data/VFS/muvfs.sql b/framework/VFS/data/VFS/muvfs.sql
new file mode 100644 (file)
index 0000000..84c0154
--- /dev/null
@@ -0,0 +1,18 @@
+-- $Horde: framework/VFS/data/VFS/muvfs.sql,v 1.3 2008/07/19 23:14:36 chuck Exp $
+
+CREATE TABLE muvfs (
+    vfs_id        INT UNSIGNED NOT NULL,
+    vfs_type      SMALLINT UNSIGNED NOT NULL,
+    vfs_path      VARCHAR(255) NOT NULL,
+    vfs_name      VARCHAR(255) NOT NULL,
+    vfs_modified  BIGINT NOT NULL,
+    vfs_owner     VARCHAR(255) NOT NULL,
+    vfs_perms     SMALLINT UNSIGNED NOT NULL,
+    vfs_data      LONGBLOB,
+-- Or, on some DBMS systems:
+--  vfs_data      IMAGE,
+    PRIMARY KEY   (vfs_id)
+);
+
+CREATE INDEX vfs_path_idx ON muvfs (vfs_path);
+CREATE INDEX vfs_name_idx ON muvfs (vfs_name);
diff --git a/framework/VFS/data/VFS/vfs.mssql.sql b/framework/VFS/data/VFS/vfs.mssql.sql
new file mode 100644 (file)
index 0000000..f3cd956
--- /dev/null
@@ -0,0 +1,15 @@
+-- $Horde: framework/VFS/data/VFS/vfs.mssql.sql,v 1.3 2008/07/19 23:14:36 chuck Exp $
+
+CREATE TABLE vfs (
+    vfs_id        INT UNSIGNED NOT NULL,
+    vfs_type      SMALLINT UNSIGNED NOT NULL,
+    vfs_path      VARCHAR(255) NOT NULL,
+    vfs_name      VARCHAR(255) NOT NULL,
+    vfs_modified  BIGINT NOT NULL,
+    vfs_owner     VARCHAR(255) NOT NULL,
+    vfs_data      VARBINARY(MAX),
+    PRIMARY KEY   (vfs_id)
+);
+
+CREATE INDEX vfs_path_idx ON vfs (vfs_path);
+CREATE INDEX vfs_name_idx ON vfs (vfs_name);
diff --git a/framework/VFS/data/VFS/vfs.oci8.sql b/framework/VFS/data/VFS/vfs.oci8.sql
new file mode 100644 (file)
index 0000000..c13d834
--- /dev/null
@@ -0,0 +1,16 @@
+-- $Horde: framework/VFS/data/VFS/vfs.oci8.sql,v 1.1 2007/12/07 00:24:22 chuck Exp $
+
+CREATE TABLE vfs (
+    vfs_id        NUMBER(16) NOT NULL,
+    vfs_type      NUMBER(8) NOT NULL,
+    vfs_path      VARCHAR2(255),
+    vfs_name      VARCHAR2(255) NOT NULL,
+    vfs_modified  NUMBER(16) NOT NULL,
+    vfs_owner     VARCHAR2(255),
+    vfs_data      BLOB,
+--
+    PRIMARY KEY   (vfs_id)
+);
+
+CREATE INDEX vfs_path_idx ON vfs (vfs_path);
+CREATE INDEX vfs_name_idx ON vfs (vfs_name);
diff --git a/framework/VFS/data/VFS/vfs.pgsql.sql b/framework/VFS/data/VFS/vfs.pgsql.sql
new file mode 100644 (file)
index 0000000..b2382f5
--- /dev/null
@@ -0,0 +1,16 @@
+-- $Horde: framework/VFS/data/VFS/vfs.pgsql.sql,v 1.4 2008/10/15 05:03:51 chuck Exp $
+
+CREATE TABLE vfs (
+    vfs_id        INT NOT NULL,
+    vfs_type      SMALLINT NOT NULL,
+    vfs_path      VARCHAR(255) NOT NULL,
+    vfs_name      VARCHAR(255) NOT NULL,
+    vfs_modified  BIGINT NOT NULL,
+    vfs_owner     VARCHAR(255) NOT NULL,
+    vfs_data      TEXT,
+
+    PRIMARY KEY   (vfs_id)
+);
+
+CREATE INDEX vfs_path_idx ON vfs (vfs_path);
+CREATE INDEX vfs_name_idx ON vfs (vfs_name);
diff --git a/framework/VFS/data/VFS/vfs.sql b/framework/VFS/data/VFS/vfs.sql
new file mode 100644 (file)
index 0000000..1e69bfd
--- /dev/null
@@ -0,0 +1,17 @@
+-- $Horde: framework/VFS/data/VFS/vfs.sql,v 1.3 2008/07/19 23:14:36 chuck Exp $
+
+CREATE TABLE vfs (
+    vfs_id        INT UNSIGNED NOT NULL,
+    vfs_type      SMALLINT UNSIGNED NOT NULL,
+    vfs_path      VARCHAR(255) NOT NULL,
+    vfs_name      VARCHAR(255) NOT NULL,
+    vfs_modified  BIGINT NOT NULL,
+    vfs_owner     VARCHAR(255) NOT NULL,
+    vfs_data      LONGBLOB,
+-- Or, on some DBMS systems:
+--  vfs_data      IMAGE,
+    PRIMARY KEY   (vfs_id)
+);
+
+CREATE INDEX vfs_path_idx ON vfs (vfs_path);
+CREATE INDEX vfs_name_idx ON vfs (vfs_name);
diff --git a/framework/VFS/lib/VFS.php b/framework/VFS/lib/VFS.php
new file mode 100644 (file)
index 0000000..50f3fa5
--- /dev/null
@@ -0,0 +1,1103 @@
+<?php
+
+require_once 'PEAR.php';
+require_once 'Log.php';
+
+define('VFS_QUOTA_METRIC_BYTE', 1);
+define('VFS_QUOTA_METRIC_KB', 2);
+define('VFS_QUOTA_METRIC_MB', 3);
+define('VFS_QUOTA_METRIC_GB', 4);
+
+/**
+ * VFS API for abstracted file storage and access.
+ *
+ * $Horde: framework/VFS/lib/VFS.php,v 1.9 2009/01/06 17:49:57 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package VFS
+ * @since   Horde 2.2
+ */
+class VFS {
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * List of additional credentials required for this VFS backend (example:
+     * For FTP, we need a username and password to log in to the server with).
+     *
+     * @var array
+     */
+    var $_credentials = array();
+
+    /**
+     * List of permissions and if they can be changed in this VFS backend.
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => false, 'write' => false, 'execute' => false),
+        'group' => array('read' => false, 'write' => false, 'execute' => false),
+        'all'   => array('read' => false, 'write' => false, 'execute' => false));
+
+    /**
+     * A PEAR Log object. If present, will be used to log errors and
+     * informational messages about VFS activity.
+     *
+     * @var Log
+     */
+    var $_logger = null;
+
+    /**
+     * The log level to use - messages with a higher log level than configured
+     * here will not be logged. Defaults to only logging errors or higher.
+     *
+     * @var integer
+     */
+    var $_logLevel = PEAR_LOG_ERR;
+
+    /**
+     * The current size, in bytes, of the VFS item.
+     *
+     * @var integer
+     */
+    var $_vfsSize = null;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  A hash containing connection parameters.
+     */
+    function VFS($params = array())
+    {
+        if (empty($params['user'])) {
+            $params['user'] = '';
+        }
+        if (empty($params['vfs_quotalimit'])) {
+            $params['vfs_quotalimit'] = -1;
+        }
+        if (empty($params['vfs_quotaroot'])) {
+            $params['vfs_quotaroot'] = '/';
+        }
+        $this->_params = $params;
+    }
+
+    /**
+     * Checks the credentials that we have by calling _connect(), to see if
+     * there is a valid login.
+     *
+     * @return mixed  True on success, PEAR_Error describing the problem if the
+     *                credentials are invalid.
+     */
+    function checkCredentials()
+    {
+        return $this->_connect();
+    }
+
+    /**
+     * Sets configuration parameters.
+     *
+     * @param array $params  An associative array with parameter names as keys.
+     */
+    function setParams($params = array())
+    {
+        foreach ($params as $name => $value) {
+            $this->_params[$name] = $value;
+        }
+    }
+
+    /**
+     * Returns configuration parameters.
+     *
+     * @param string $name  The parameter to return.
+     *
+     * @return mixed  The parameter value or null if it doesn't exist.
+     */
+    function getParam($name)
+    {
+        return isset($this->_params[$name]) ? $this->_params[$name] : null;
+    }
+
+    /**
+     * Logs a message if a PEAR Log object is available, and the message's
+     * priority is lower than or equal to the configured log level.
+     *
+     * @param mixed   $message   The message to be logged.
+     * @param integer $priority  The message's priority.
+     */
+    function log($message, $priority = PEAR_LOG_ERR)
+    {
+        if (!isset($this->_logger) || $priority > $this->_logLevel) {
+            return;
+        }
+
+        if (is_a($message, 'PEAR_Error')) {
+            $userinfo = $message->getUserInfo();
+            $message = $message->getMessage();
+            if ($userinfo) {
+                if (is_array($userinfo)) {
+                    $userinfo = implode(', ', $userinfo);
+                }
+                $message .= ': ' . $userinfo;
+            }
+        }
+
+        /* Make sure to log in the system's locale. */
+        $locale = setlocale(LC_TIME, 0);
+        setlocale(LC_TIME, 'C');
+
+        $this->_logger->log($message, $priority);
+
+        /* Restore original locale. */
+        setlocale(LC_TIME, $locale);
+    }
+
+    /**
+     * Sets the PEAR Log object used to log informational or error messages.
+     *
+     * @param Log &$logger  The Log object to use.
+     */
+    function setLogger(&$logger, $logLevel = null)
+    {
+        if (!is_callable(array($logger, 'log'))) {
+            return false;
+        }
+
+        $this->_logger = &$logger;
+        if (!is_null($logLevel)) {
+            $this->_logLevel = $logLevel;
+        }
+    }
+
+    /**
+     * Retrieves the size of a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return integer The file size.
+     */
+    function size($path, $name)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns the size of a folder
+     *
+     * @since Horde 3.1
+     *
+     * @param string $path  The path to the folder.
+     * @param string $name  The name of the folder.
+     *
+     * @return integer  The size of the folder, in bytes, or PEAR_Error on
+     *                  failure.
+     */
+    function getFolderSize($path = null, $name = null)
+    {
+        $size = 0;
+        $root = ((!is_null($path)) ? $path . '/' : '') . $name;
+        $object_list = $this->listFolder($root, null, true, false, true);
+        foreach ($object_list as $key => $val) {
+            if (isset($val['subdirs'])) {
+                $size += $this->getFolderSize($root, $key);
+            } else {
+                $filesize = $this->size($root, $key);
+                if (is_a($filesize, 'PEAR_Error')) {
+                    return $filesize;
+                }
+                $size += $filesize;
+            }
+        }
+
+        return $size;
+    }
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string The file data.
+     */
+    function read($path, $name)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Retrieves a file from the VFS as an on-disk local file.
+     *
+     * This function provides a file on local disk with the data of a VFS file
+     * in it. This file <em>cannot</em> be modified! The behavior if you do
+     * modify it is undefined. It will be removed at the end of the request.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string A local filename.
+     */
+    function readFile($path, $name)
+    {
+        // Create a temporary file and register it for deletion at the
+        // end of this request.
+        $localFile = $this->_getTempFile();
+        if (!$localFile) {
+            return PEAR::raiseError(_("Unable to create temporary file."));
+        }
+        register_shutdown_function(create_function('', 'unlink(\'' . addslashes($localFile) . '\');'));
+
+        if (is_callable(array($this, 'readStream'))) {
+            // Use a stream from the VFS if possible, to avoid reading all data
+            // into memory.
+            $stream = $this->readStream($path, $name);
+            if (is_a($stream, 'PEAR_Error')) {
+                return $stream;
+            }
+
+            $localStream = fopen($localFile, 'w');
+            if (!$localStream) {
+                return PEAR::raiseError(_("Unable to open temporary file."));
+            }
+
+            if (is_callable('stream_copy_to_stream')) {
+                // If we have stream_copy_to_stream, it can do the data transfer
+                // in one go.
+                stream_copy_to_stream($stream, $localStream);
+            } else {
+                // Otherwise loop through in chunks.
+                while ($buffer = fread($stream, 8192)) {
+                    fwrite($localStream, $buffer);
+                }
+            }
+
+            fclose($localStream);
+        } else {
+            // We have to read all of the data in one shot.
+            $data = $this->read($path, $name);
+            if (is_a($data, 'PEAR_Error')) {
+                return $data;
+            }
+
+            if (is_callable('file_put_contents')) {
+                // file_put_contents is more efficient if we have it.
+                file_put_contents($localFile, $data);
+            } else {
+                // Open the local file and write to it.
+                $localStream = fopen($localFile, 'w');
+                if (!$localStream) {
+                    return PEAR::raiseError(_("Unable to open temporary file."));
+                }
+                if (!fwrite($localStream, $data)) {
+                    return PEAR::raiseError(_("Unable to write temporary file."));
+                }
+                fclose($localStream);
+            }
+        }
+
+        // $localFile now has $path/$name's data in it.
+        return $localFile;
+    }
+
+    /**
+     * Retrieves a part of a file from the VFS. Particularly useful when
+     * reading large files which would exceed the PHP memory limits if they
+     * were stored in a string.
+     *
+     * @abstract
+     *
+     * @param string  $path       The pathname to the file.
+     * @param string  $name       The filename to retrieve.
+     * @param integer $offset     The offset of the part. (The new offset will
+     *                            be stored in here).
+     * @param integer $length     The length of the part. If the length = -1,
+     *                            the whole part after the offset is retrieved.
+     *                            If more bytes are given as exists after the
+     *                            given offset. Only the available bytes are
+     *                            read.
+     * @param integer $remaining  The bytes that are left, after the part that
+     *                            is retrieved.
+     *
+     * @return string The file data.
+     */
+    function readByteRange($path, $name, &$offset, $length = -1, &$remaining)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @abstract
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Moves a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The destination file name.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        if (is_a($result = $this->copy($path, $name, $dest, $autocreate), 'PEAR_Error')) {
+            return $result;
+        }
+        return $this->deleteFile($path, $name);
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The name of the destination directory.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        if ($this->isFolder($path, $name)) {
+            if (is_a($result = $this->_copyRecursive($path, $name, $dest), 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $data = $this->read($path, $name);
+            if (is_a($data, 'PEAR_Error')) {
+                return $data;
+            }
+            return $this->writeData($dest, $name, $data, $autocreate);
+        }
+        return true;
+    }
+
+    /**
+     * Recursively copies a directory through the backend.
+     *
+     * @access protected
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The name of the destination directory.
+     */
+    function _copyRecursive($path, $name, $dest)
+    {
+        if (is_a($result = $this->createFolder($dest, $name), 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (is_a($file_list = $this->listFolder($this->_getPath($path, $name)), 'PEAR_Error')) {
+            return $file_list;
+        }
+
+        foreach ($file_list as $file) {
+            $result = $this->copy($this->_getPath($path, $name),
+                                  $file['name'],
+                                  $this->_getPath($dest, $name));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+    }
+
+    /**
+     * Alias to deleteFile()
+     */
+    function delete($path, $name)
+    {
+        return $this->deleteFile($path, $name);
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Renames a file in the VFS.
+     *
+     * @abstract
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns if a given file or folder exists in a folder.
+     *
+     * @param string $path  The path to the folder.
+     * @param string $name  The file or folder name.
+     *
+     * @return boolean  True if it exists, false otherwise.
+     */
+    function exists($path, $name)
+    {
+        $list = $this->listFolder($path);
+        if (is_a($list, 'PEAR_Error')) {
+            return false;
+        } else {
+            return isset($list[$name]);
+        }
+    }
+
+    /**
+     * Creates a folder in the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Automatically creates any necessary parent directories in the specified
+     * $path.
+     *
+     * @param string $path  The VFS path to autocreate.
+     */
+    function autocreatePath($path)
+    {
+        $dirs = explode('/', $path);
+        if (is_array($dirs)) {
+            $cur = '/';
+            foreach ($dirs as $dir) {
+                if (!strlen($dir)) {
+                    continue;
+                }
+                if (!$this->isFolder($cur, $dir)) {
+                    $result = $this->createFolder($cur, $dir);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                }
+                if ($cur != '/') {
+                    $cur .= '/';
+                }
+                $cur .= $dir;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if a given item is a folder.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The item name.
+     *
+     * @return boolean  True if it is a folder, false otherwise.
+     */
+    function isFolder($path, $name)
+    {
+        $folderList = $this->listFolder($path, null, true, true);
+        return isset($folderList[$name]);
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path        The parent folder.
+     * @param string $name        The name of the folder to delete.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Recursively remove all files and subfolders from the given
+     * folder.
+     *
+     * @param string $path  The path of the folder to empty.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function emptyFolder($path)
+    {
+        // Get and delete the subfolders.
+        $list = $this->listFolder($path, null, true, true);
+        if (is_a($list, 'PEAR_Error')) {
+            return $list;
+        }
+        foreach ($list as $folder) {
+            $result = $this->deleteFolder($path, $folder['name'], true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        // Only files are left, get and delete them.
+        $list = $this->listFolder($path);
+        if (is_a($list, 'PEAR_Error')) {
+            return $list;
+        }
+        foreach ($list as $file) {
+            $result = $this->deleteFile($path, $file['name']);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns a file list of the directory passed in.
+     *
+     * @param string $path        The path of the directory.
+     * @param mixed $filter       String/hash to filter file/dirname on.
+     * @param boolean $dotfiles   Show dotfiles?
+     * @param boolean $dironly    Show only directories?
+     * @param boolean $recursive  Return all directory levels recursively?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function listFolder($path, $filter = null, $dotfiles = true,
+                        $dironly = false, $recursive = false)
+    {
+        $list = $this->_listFolder($path, $filter, $dotfiles, $dironly);
+        if (!$recursive || is_a($list, 'PEAR_Error')) {
+            return $list;
+        }
+
+        foreach ($list as $name => $values) {
+            if ($values['type'] == '**dir') {
+                $list[$name]['subdirs'] = $this->listFolder($path . '/' . $name, $filter, $dotfiles, $dironly, $recursive);
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Returns an an unsorted file list of the specified directory.
+     *
+     * @abstract
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns the current working directory of the VFS backend.
+     *
+     * @return string  The current working directory.
+     */
+    function getCurrentDirectory()
+    {
+        return '';
+    }
+
+    /**
+     * Returns whether or not a filename matches any filter element.
+     *
+     * @access private
+     *
+     * @param mixed $filter     String/hash to build the regular expression
+     *                          from.
+     * @param string $filename  String containing the filename to match.
+     *
+     * @return boolean  True on match, false on no match.
+     */
+    function _filterMatch($filter, $filename)
+    {
+        $namefilter = null;
+
+        // Build a regexp based on $filter.
+        if ($filter !== null) {
+            $namefilter = '/';
+            if (is_array($filter)) {
+                $once = false;
+                foreach ($filter as $item) {
+                    if ($once !== true) {
+                        $namefilter .= '(';
+                        $once = true;
+                    } else {
+                        $namefilter .= '|(';
+                    }
+                    $namefilter .= $item . ')';
+                }
+            } else {
+                $namefilter .= '(' . $filter . ')';
+            }
+            $namefilter .= '/';
+        }
+
+        $match = false;
+        if ($namefilter !== null) {
+            $match = preg_match($namefilter, $filename);
+        }
+
+        return $match;
+    }
+
+    /**
+     * Changes permissions for an item on the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path        The parent folder of the item.
+     * @param string $name        The name of the item.
+     * @param string $permission  The permission to set.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $name, $permission)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns a sorted list of folders in the specified directory.
+     *
+     * @abstract
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns the list of additional credentials required, if any.
+     *
+     * @return array  Credential list.
+     */
+    function getRequiredCredentials()
+    {
+        return array_diff($this->_credentials, array_keys($this->_params));
+    }
+
+    /**
+     * Returns an array specifying what permissions are changeable for this
+     * VFS implementation.
+     *
+     * @return array  Changeable permisions.
+     */
+    function getModifiablePermissions()
+    {
+        return $this->_permissions;
+    }
+
+    /**
+     * Converts a string to all lowercase characters ignoring the current
+     * locale.
+     *
+     * @param string $string  The string to be lowercased
+     *
+     * @return string  The string with lowercase characters
+     */
+    function strtolower($string)
+    {
+        $language = setlocale(LC_CTYPE, 0);
+        setlocale(LC_CTYPE, 'C');
+        $string = strtolower($string);
+        setlocale(LC_CTYPE, $language);
+        return $string;
+    }
+
+    /**
+     * Returns the character (not byte) length of a string.
+     *
+     * @param string $string   The string to return the length of.
+     * @param string $charset  The charset to use when calculating the
+     *                         string's length.
+     *
+     * @return string  The string's length.
+     */
+    function strlen($string, $charset = null)
+    {
+        if (extension_loaded('mbstring')) {
+            if (is_null($charset)) {
+                $charset = 'ISO-8859-1';
+            }
+            $result = @mb_strlen($string, $charset);
+            if (!empty($result)) {
+                return $result;
+            }
+        }
+        return strlen($string);
+    }
+
+    /**
+     * Returns the size of the VFS item.
+     *
+     * @since Horde 3.1
+     *
+     * @return integer  The size, in bytes, of the VFS item.
+     */
+    function getVFSSize()
+    {
+        if (is_null($this->_vfsSize)) {
+            $this->_vfsSize = $this->getFolderSize($this->_params['vfs_quotaroot']);
+        }
+        return $this->_vfsSize;
+    }
+
+    /**
+     * Sets the VFS quota limit.
+     *
+     * @since Horde 3.1
+     *
+     * @param integer $quota   The limit to apply.
+     * @param integer $metric  The metric to multiply the quota into.
+     */
+    function setQuota($quota, $metric = VFS_QUOTA_METRIC_BYTE)
+    {
+        switch ($metric) {
+        case VFS_QUOTA_METRIC_KB:
+            $quota *= pow(2, 10);
+            break;
+
+        case VFS_QUOTA_METRIC_MB:
+            $quota *= pow(2, 20);
+            break;
+
+        case VFS_QUOTA_METRIC_GB:
+            $quota *= pow(2, 30);
+            break;
+        }
+
+        $this->_params['vfs_quotalimit'] = $quota;
+    }
+
+    /**
+     * Sets the VFS quota root.
+     *
+     * @since Horde 3.1
+     *
+     * @param string $dir  The root directory for the quota determination.
+     */
+    function setQuotaRoot($dir)
+    {
+        $this->_params['vfs_quotaroot'] = $dir;
+    }
+
+    /**
+     * Get quota information (used/allocated), in bytes.
+     *
+     * @since Horde 3.1
+     *
+     * @return mixed  An associative array.
+     *                'limit' = Maximum quota allowed
+     *                'usage' = Currently used portion of quota (in bytes)
+     *                Returns PEAR_Error on failure.
+     */
+    function getQuota()
+    {
+        if (empty($this->_params['vfs_quotalimit'])) {
+            return PEAR::raiseError(_("No quota set."));
+        }
+
+        $usage = $this->getVFSSize();
+        if (is_a($usage, 'PEAR_Error')) {
+            return $usage;
+        } else {
+            return array('usage' => $usage, 'limit' => $this->_params['vfs_quotalimit']);
+        }
+    }
+
+    /**
+     * Determines the location of the system temporary directory.
+     *
+     * @access protected
+     *
+     * @return string  A directory name which can be used for temp files.
+     *                 Returns false if one could not be found.
+     */
+    function _getTempDir()
+    {
+        $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp',
+                               'c:\windows\temp', 'c:\winnt\temp');
+
+        /* Try PHP's upload_tmp_dir directive. */
+        $tmp = ini_get('upload_tmp_dir');
+
+        /* Otherwise, try to determine the TMPDIR environment variable. */
+        if (!strlen($tmp)) {
+            $tmp = getenv('TMPDIR');
+        }
+
+        /* If we still cannot determine a value, then cycle through a list of
+         * preset possibilities. */
+        while (!strlen($tmp) && count($tmp_locations)) {
+            $tmp_check = array_shift($tmp_locations);
+            if (@is_dir($tmp_check)) {
+                $tmp = $tmp_check;
+            }
+        }
+
+        /* If it is still empty, we have failed, so return false; otherwise
+         * return the directory determined. */
+        return strlen($tmp) ? $tmp : false;
+    }
+
+    /**
+     * Creates a temporary file.
+     *
+     * @access protected
+     *
+     * @return string   Returns the full path-name to the temporary file or
+     *                  false if a temporary file could not be created.
+     */
+    function _getTempFile()
+    {
+        $tmp_dir = $this->_getTempDir();
+        if (!strlen($tmp_dir)) {
+            return false;
+        }
+
+        $tmp_file = tempnam($tmp_dir, 'vfs');
+        if (!strlen($tmp_file)) {
+            return false;
+        } else {
+            return $tmp_file;
+        }
+    }
+
+    /**
+     * Checks the quota when preparing to write data.
+     *
+     * @access private
+     *
+     * @param string $mode   Either 'string' or 'file'.  If 'string', $data is
+     *                       the data to be written.  If 'file', $data is the
+     *                       filename containing the data to be written.
+     * @param string $data   Either the data or the filename to the data.
+     *
+     * @return mixed  PEAR_Error on error, true on success.
+     */
+    function _checkQuotaWrite($mode, $data)
+    {
+        if ($this->_params['vfs_quotalimit'] != -1) {
+            if ($mode == 'file') {
+                $filesize = filesize($data);
+                if ($filesize === false) {
+                    return PEAR::raiseError(_("Unable to read VFS file (filesize() failed)."));
+               }
+            } else {
+                $filesize = strlen($data);
+            }
+            $vfssize = $this->getVFSSize();
+            if (is_a($vfssize, 'PEAR_Error')) {
+                return $vfssize;
+            }
+            if (($vfssize + $filesize) > $this->_params['vfs_quotalimit']) {
+                return PEAR::raiseError(_("Unable to write VFS file, quota will be exceeded."));
+            } elseif ($this->_vfsSize !== 0) {
+                $this->_vfsSize += $filesize;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks the quota when preparing to delete data.
+     *
+     * @access private
+     *
+     * @param string $path  The path the file is located in.
+     * @param string $name  The filename.
+     *
+     * @return mixed  PEAR_Error on error, true on success.
+     */
+    function _checkQuotaDelete($path, $name)
+    {
+        if (($this->_params['vfs_quotalimit'] != -1) &&
+            !empty($this->_vfsSize)) {
+            $filesize = $this->size($path, $name);
+            if (is_a($filesize, 'PEAR_Error')) {
+                return PEAR::raiseError(_("Unable to read VFS file (size() failed)."));
+            }
+            $this->_vfsSize -= $filesize;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the full path of an item.
+     *
+     * @access protected
+     *
+     * @param string $path  The path of directory of the item.
+     * @param string $name  The name of the item.
+     *
+     * @return mixed  Full path when $path isset and just $name when not set.
+     */
+    function _getPath($path, $name)
+    {
+        if (strlen($path) > 0) {
+            if (substr($path, -1) == '/') {
+                return $path . $name;
+            } else {
+                return $path . '/' . $name;
+            }
+        } else {
+            return $name;
+        }
+    }
+
+    /**
+     * Attempts to return a concrete VFS instance based on $driver.
+     *
+     * @param mixed $driver  The type of concrete VFS subclass to return. This
+     *                       is based on the storage driver ($driver). The
+     *                       code is dynamically included.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return VFS  The newly created concrete VFS instance, or a PEAR_Error
+     *              on failure.
+     */
+    function &factory($driver, $params = array())
+    {
+        $driver = basename($driver);
+        $class = 'VFS_' . $driver;
+        if (!class_exists($class)) {
+            include_once 'VFS/' . $driver . '.php';
+        }
+
+        if (class_exists($class)) {
+            $vfs = new $class($params);
+        } else {
+            $vfs = PEAR::raiseError(sprintf(_("Class definition of %s not found."), $class));
+        }
+
+        return $vfs;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete VFS instance based on
+     * $driver. It will only create a new instance if no VFS instance with the
+     * same parameters currently exists.
+     *
+     * This should be used if multiple types of file backends (and, thus,
+     * multiple VFS instances) are required.
+     *
+     * This method must be invoked as: $var = &VFS::singleton()
+     *
+     * @param mixed $driver  The type of concrete VFS subclass to return. This
+     *                       is based on the storage driver ($driver). The
+     *                       code is dynamically included.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return VFS  The concrete VFS reference, or a PEAR_Error on failure.
+     */
+    function &singleton($driver, $params = array())
+    {
+        static $instances = array();
+
+        $signature = serialize(array($driver, $params));
+        if (!isset($instances[$signature])) {
+            $instances[$signature] = &VFS::factory($driver, $params);
+        }
+
+        return $instances[$signature];
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/Browser.php b/framework/VFS/lib/VFS/Browser.php
new file mode 100644 (file)
index 0000000..83b3038
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Class for providing a generic UI for any VFS instance.
+ *
+ * $Horde: framework/VFS/lib/VFS/Browser.php,v 1.3 2009/01/06 17:49:58 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package VFS
+ */
+class VFS_Browser {
+
+    /**
+     * The VFS instance that we are browsing.
+     *
+     * @var VFS
+     */
+    var $_vfs;
+
+    /**
+     * The directory where the templates to use are.
+     *
+     * @var string
+     */
+    var $_templates;
+
+    /**
+     * Constructor
+     *
+     * @param VFS &$vfs          A VFS object.
+     * @param string $templates  TODO
+     */
+    function VFS_Browser(&$vfs, $templates)
+    {
+        if (isset($vfs)) {
+            $this->_vfs = $vfs;
+        }
+        $this->_templates = $templates;
+    }
+
+    /**
+     * Set the VFS object in the local object.
+     *
+     * @param VFS &$vfs  A VFS object.
+     */
+    function setVFSObject(&$vfs)
+    {
+        $this->_vfs = &$vfs;
+    }
+
+    /**
+     * TODO
+     *
+     * @param string $path       TODO
+     * @param boolean $dotfiles  TODO
+     * @param boolean $dironly   TODO
+     */
+    function getUI($path, $dotfiles = false, $dironly = false)
+    {
+        $this->_vfs->listFolder($path, $dotfiles, $dironly);
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/GC.php b/framework/VFS/lib/VFS/GC.php
new file mode 100644 (file)
index 0000000..c8d8f14
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Class for providing garbage collection for any VFS instance.
+ *
+ * $Horde: framework/VFS/lib/VFS/GC.php,v 1.3 2009/01/06 17:49:58 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package VFS
+ */
+class VFS_GC {
+
+    /**
+     * Garbage collect files in the VFS storage system.
+     *
+     * @param VFS &$vfs      The VFS object to perform garbage collection on.
+     * @param string $path   The VFS path to clean.
+     * @param integer $secs  The minimum amount of time (in seconds) required
+     *                       before a file is removed.
+     */
+    function gc(&$vfs, $path, $secs = 345600)
+    {
+        /* A 1% chance we will run garbage collection during a call. */
+        if (rand(0, 99) != 0) {
+            return;
+        }
+
+        /* Use a backend-specific method if one exists. */
+        if (is_callable(array($vfs, 'gc'))) {
+            return $vfs->gc($path, $secs);
+        }
+
+        /* Make sure cleaning is done recursively. */
+        $files = $vfs->listFolder($path, null, true, false, true);
+        if (!is_a($files, 'PEAR_Error') && is_array($files)) {
+            $modtime = time() - $secs;
+            foreach ($files as $val) {
+                if ($val['date'] < $modtime) {
+                    $vfs->deleteFile($path, $val['name']);
+                }
+            }
+        }
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/ListItem.php b/framework/VFS/lib/VFS/ListItem.php
new file mode 100644 (file)
index 0000000..58a09f1
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * An item returned from a folder list.
+ *
+ * $Horde: framework/VFS/lib/VFS/ListItem.php,v 1.1 2007/12/07 00:24:22 chuck Exp $
+ *
+ * Copyright 2002-2007 Jon Wood <jon@jellybob.co.uk>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jon Wood <jon@jellybob.co.uk>
+ * @package VFS
+ */
+class VFS_ListItem {
+
+    /**
+     * VFS path
+     *
+     * @var string
+     */
+    var $_path;
+
+    /**
+     * Filename
+     *
+     * @var string
+     */
+    var $_name;
+
+    /**
+     * File permissions (*nix format: drwxrwxrwx)
+     *
+     * @var string
+     */
+    var $_perms;
+
+    /**
+     * Owner user
+     *
+     * @var string
+     */
+    var $_owner;
+
+    /**
+     * Owner group
+     *
+     * @var string
+     */
+    var $_group;
+
+    /**
+     * Size.
+     *
+     * @var string
+     */
+    var $_size;
+
+    /**
+     * Last modified date.
+     *
+     * @var string
+     */
+    var $_date;
+
+    /**
+     * Type
+     *   .*      --  File extension
+     *   **none  --  Unrecognized type
+     *   **sym   --  Symlink
+     *   **dir   --  Directory
+     *
+     * @var string
+     */
+    var $_type;
+
+    /**
+     * Type of target if type is '**sym'.
+     * NB. Not all backends are capable of distinguishing all of these.
+     *   .*        --  File extension
+     *   **none    --  Unrecognized type
+     *   **sym     --  Symlink to a symlink
+     *   **dir     --  Directory
+     *   **broken  --  Target not found - broken link
+     *
+     * @var string
+     */
+    var $_linktype;
+
+    /**
+     * Constructor
+     *
+     * Requires the path to the file, and it's array of properties,
+     * returned from a standard VFS::listFolder() call.
+     *
+     * @param string $path      The path to the file.
+     * @param array $fileArray  An array of file properties.
+     */
+    function VFS_ListItem($path, $fileArray)
+    {
+        $this->_path = $path . '/' . $fileArray['name'];
+        $this->_name = $fileArray['name'];
+        $this->_dirname = $path;
+        $this->_perms = $fileArray['perms'];
+        $this->_owner = $fileArray['owner'];
+        $this->_group = $fileArray['group'];
+        $this->_size = $fileArray['size'];
+        $this->_date = $fileArray['date'];
+        $this->_type = $fileArray['type'];
+        $this->_linktype = $fileArray['linktype'];
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/Object.php b/framework/VFS/lib/VFS/Object.php
new file mode 100644 (file)
index 0000000..1e281ab
--- /dev/null
@@ -0,0 +1,286 @@
+<?php
+
+require_once dirname(__FILE__) . '/../VFS.php';
+
+/**
+ * A wrapper for the VFS class to return objects, instead of arrays.
+ *
+ * $Horde: framework/VFS/lib/VFS/Object.php,v 1.2 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2002-2007 Jon Wood <jon@jellybob.co.uk>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jon Wood <jon@jellybob.co.uk>
+ * @package VFS
+ */
+class VFS_Object {
+
+    /**
+     * The actual vfs that does the work
+     *
+     * @var VFS
+     */
+    var $_vfs;
+
+    /**
+     * The current path that has been passed to listFolder, if this
+     * changes, the list will be rebuilt.
+     *
+     * @var string
+     */
+    var $_currentPath;
+
+    /**
+     * The return value from a standard VFS listFolder call, to
+     * be read with the Object listFolder.
+     *
+     * @var array
+     */
+    var $_folderList;
+
+    /**
+     * Constructor.
+     *
+     * If you pass in an existing VFS object, it will be used as the VFS
+     * object for this object.
+     *
+     * @param VFS &$vfs  The VFS object to wrap.
+     */
+    function VFS_Object(&$vfs)
+    {
+        if (isset($vfs)) {
+            $this->_vfs = $vfs;
+        }
+    }
+
+    /**
+     * Attempts to return a concrete VFS_Object instance based on $driver.
+     *
+     * @param mixed $driver  The type of concrete VFS subclass to return. If
+     *                       $driver is an array then we will look in
+     *                       $driver[0]/lib/VFS/ for the subclass
+     *                       implementation named $driver[1].php.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return VFS_Object  The newly created concrete VFS_Object instance, or
+     *                     false on an error.
+     */
+    function &factory($driver, $params = array())
+    {
+        $vfs = &VFS::factory($driver, $params = array());
+        $vfsobject = new VFS_Object($vfs);
+        return $vfsobject;
+    }
+
+    /**
+     * Attempts to return a reference to a concrete VFS instance
+     * based on $driver. It will only create a new instance if no
+     * VFS instance with the same parameters currently exists.
+     *
+     * This should be used if multiple types of file backends (and,
+     * thus, multiple VFS instances) are required.
+     *
+     * This method must be invoked as: $var = &VFS::singleton()
+     *
+     * @param mixed $driver  The type of concrete VFS subclass to return. If
+     *                       $driver is an array then we will look in
+     *                       $driver[0]/lib/VFS/ for the subclass
+     *                       implementation named $driver[1].php.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return VFS_Object  The concrete VFS_Object reference, or false on
+     *                     error.
+     */
+    function &singleton($driver, $params = array())
+    {
+        $vfs = &VFS::singleton($driver, $params = array());
+        $vfsobject = new VFS_Object($vfs);
+        return $vfsobject;
+    }
+
+    /**
+     * Check the credentials that we have to see if there is a valid login.
+     *
+     * @return mixed  True on success, PEAR_Error describing the problem
+     *                if the credentials are invalid.
+     */
+    function checkCredentials()
+    {
+        return $this->_vfs->checkCredentials();
+    }
+
+    /**
+     * Set configuration parameters.
+     *
+     * @param array $params  An associative array of parameter name/value
+     *                       pairs.
+     */
+    function setParams($params = array())
+    {
+        $this->_vfs->setParams($params);
+    }
+
+    /**
+     * Retrieve a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     *
+     * @return string  The file data.
+     */
+    function read($path)
+    {
+        return $this->_vfs->read(dirname($path), basename($path));
+    }
+
+    /**
+     * Store a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $tmpFile      The temporary file containing the data to be
+     *                             stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $tmpFile, $autocreate = false)
+    {
+        return $this->_vfs->write(dirname($path), basename($path), $tmpFile, $autocreate = false);
+    }
+
+    /**
+     * Store a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $data, $autocreate = false)
+    {
+        return $this->_vfs->writeData(dirname($path), basename($path), $data, $autocreate = false);
+    }
+
+    /**
+     * Delete a file from the VFS.
+     *
+     * @param string $path  The path to store the file in.
+     * @param string $name  The filename to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path)
+    {
+        return $this->_vfs->deleteFile(dirname($path), basename($path));
+    }
+
+    /**
+     * Rename a file in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $newpath)
+    {
+        return $this->_vfs->rename(dirname($oldpath), basename($oldpath), dirname($newpath), basename($newpath));
+    }
+
+    /**
+     * Create a folder in the VFS.
+     *
+     * @param string $path  The path to the folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path)
+    {
+        return $this->_vfs->createFolder(dirname($path));
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path The path of the folder to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path)
+    {
+        return $this->_vfs->deleteFolder(dirname($path));
+    }
+
+    /**
+     * Returns a VFS_ListItem object if the folder can
+     * be read, or a PEAR_Error if it can't be. Returns false once
+     * the folder has been completely read.
+     *
+     * @param string $path  The path of the diretory.
+     *
+     * @return mixed  File list (array) on success, a PEAR_Error
+     *                object on failure, or false if the folder is
+     *                completely read.
+     */
+    function listFolder($path)
+    {
+        if (!($path === $this->_currentPath)) {
+            $folderList = $this->_vfs->listFolder($path);
+            if ($folderList) {
+                $this->_folderList = $folderList;
+                $this->_currentPath = $path;
+            } else {
+                return PEAR::raiseError(sprintf(_("Could not read %s."), $path));
+            }
+        }
+
+        require_once dirname(__FILE__) . '/ListItem.php';
+        if ($file = array_shift($this->_folderList)) {
+            $file = new VFS_ListItem($path, $file);
+            return $file;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Changes permissions for an Item on the VFS.
+     *
+     * @param string $path        Holds the path of directory of the Item.
+     * @param string $permission  TODO
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $permission)
+    {
+        return $this->_vfs->changePermissions(dirname($path), basename($path), $permission);
+    }
+
+    /**
+     * Return the list of additional credentials required, if any.
+     *
+     * @return array  Credential list.
+     */
+    function getRequiredCredentials()
+    {
+        return $this->_vfs->getRequiredCredentials();
+    }
+
+    /**
+     * Return the array specificying what permissions are changeable for this
+     * implementation.
+     *
+     * @return array  Changeable permisions.
+     */
+    function getModifiablePermissions()
+    {
+        return $this->_vfs->getModifiablePermissions();
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/file.php b/framework/VFS/lib/VFS/file.php
new file mode 100644 (file)
index 0000000..f87f9ef
--- /dev/null
@@ -0,0 +1,768 @@
+<?php
+/**
+ * VFS implementation for a standard filesystem.
+ *
+ * Required parameters:<pre>
+ *   'vfsroot'  The root path</pre>
+ *
+ * Note: The user that your webserver runs as (commonly 'nobody',
+ * 'apache', or 'www-data') MUST have read/write permission to the
+ * directory you specify as the 'vfsroot'.
+ *
+ * $Horde: framework/VFS/lib/VFS/file.php,v 1.6 2009/10/15 17:13:28 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch
+ * @package VFS
+ */
+class VFS_file extends VFS {
+
+    /**
+     * List of permissions and if they can be changed in this VFS
+     * backend.
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => true, 'write' => true, 'execute' => true),
+        'group' => array('read' => true, 'write' => true, 'execute' => true),
+        'all'   => array('read' => true, 'write' => true, 'execute' => true)
+    );
+
+    /**
+     * Constructs a new Filesystem based VFS object.
+     *
+     * @param array $params  A hash containing connection parameters.
+     */
+    function VFS_file($params = array())
+    {
+        parent::VFS($params);
+
+        if (!empty($this->_params['vfsroot'])) {
+            if (substr($this->_params['vfsroot'], -1) == '/' ||
+                substr($this->_params['vfsroot'], -1) == '\\') {
+                $this->_params['vfsroot'] = substr($this->_params['vfsroot'], 0, strlen($this->_params['vfsroot']) - 1);
+            }
+        }
+    }
+
+    /**
+     * Retrieves the filesize from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return integer The file size.
+     */
+    function size($path, $name)
+    {
+        $size = @filesize($this->_getNativePath($path, $name));
+        if ($size === false) {
+            return PEAR::raiseError(sprintf(_("Unable to check file size of \"%s/%s\"."), $path, $name));
+        }
+        return $size;
+    }
+
+    /**
+     * Retrieve a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        $data = @file_get_contents($this->_getNativePath($path, $name));
+        if ($data === false) {
+            return PEAR::raiseError(_("Unable to open VFS file."));
+        }
+
+        return $data;
+    }
+
+    /**
+     * Retrieves a file from the VFS as an on-disk local file.
+     *
+     * This function provides a file on local disk with the data of a VFS file
+     * in it. This file <em>cannot</em> be modified! The behavior if you do
+     * modify it is undefined. It will be removed at the end of the request.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string A local filename.
+     */
+    function readFile($path, $name)
+    {
+        return $this->_getNativePath($path, $name);
+    }
+
+    /**
+     * Open a read-only stream to a file in the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return resource  The stream.
+     */
+    function readStream($path, $name)
+    {
+        $mode = OS_WINDOWS ? 'rb' : 'r';
+        $stream = @fopen($this->_getNativePath($path, $name), $mode);
+        if (!is_resource($stream)) {
+            return PEAR::raiseError(_("Unable to open VFS file."));
+        }
+
+        return $stream;
+    }
+
+    /**
+     * Retrieves a part of a file from the VFS. Particularly useful when
+     * reading large files which would exceed the PHP memory limits if they
+     * were stored in a string.
+     *
+     * @abstract
+     *
+     * @param string  $path       The pathname to the file.
+     * @param string  $name       The filename to retrieve.
+     * @param integer $offset     The offset of the part. (The new offset will
+     *                            be stored in here).
+     * @param integer $length     The length of the part. If the length = -1,
+     *                            the whole part after the offset is retrieved.
+     *                            If more bytes are given as exists after the
+     *                            given offset. Only the available bytes are
+     *                            read.
+     * @param integer $remaining  The bytes that are left, after the part that
+     *                            is retrieved.
+     *
+     * @return string The file data.
+     */
+    function readByteRange($path, $name, &$offset, $length = -1, &$remaining)
+    {
+        if ($offset < 0) {
+            return PEAR::raiseError(sprintf(_("Wrong offset %d while reading a VFS file."), $offset));
+        }
+
+        // Calculate how many bytes MUST be read, so the remainging
+        // bytes and the new offset can be calculated correctly.
+        $file = $this->_getNativePath($path, $name);
+        $size = filesize ($file);
+        if ($length == -1 || (($length + $offset) > $size)) {
+            $length = $size - $offset;
+        }
+        if ($remaining < 0) {
+            $remaining = 0;
+        }
+
+        $fp = @fopen($file, 'rb');
+        if (!$fp) {
+            return PEAR::raiseError(_("Unable to open VFS file."));
+        }
+        fseek($fp, $offset);
+        $data = fread($fp, $length);
+        $offset = ftell($fp);
+        $remaining = $size - $offset;
+
+        fclose($fp);
+
+        return $data;
+    }
+
+    /**
+     * Store a file in the VFS, with the data copied from a temporary
+     * file.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to be
+     *                             stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = true)
+    {
+        if (!@is_dir($this->_getNativePath($path))) {
+            if ($autocreate) {
+                $res = $this->autocreatePath($path);
+                if (is_a($res, 'PEAR_Error')) {
+                    return $res;
+                }
+            } else {
+                return PEAR::raiseError(_("VFS directory does not exist."));
+            }
+        }
+
+        $res = $this->_checkQuotaWrite('file', $tmpFile);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        // Since we already have the data in a file, don't read it
+        // into PHP's memory at all - just copy() it to the new
+        // location. We leave it to the caller to clean up the
+        // temporary file, so we don't use rename().
+        if (@copy($tmpFile, $this->_getNativePath($path, $name))) {
+            return true;
+        } else {
+            return PEAR::raiseError(_("Unable to write VFS file (copy() failed)."));
+        }
+    }
+
+    /**
+     * Moves a file in the database and the file system.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $dest         The destination of the file.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getNativePath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, false);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(_("Unable to move VFS file."));
+            }
+        }
+
+        if (!@rename($orig, $this->_getNativePath($dest, $name))) {
+            return PEAR::raiseError(_("Unable to move VFS file."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $dest         The destination of the file.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getNativePath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, false);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(_("Unable to copy VFS file."));
+            }
+        }
+
+        $res = $this->_checkQuotaWrite('file', $orig);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!@copy($orig, $this->_getNativePath($dest, $name))) {
+            return PEAR::raiseError(_("Unable to copy VFS file."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Store a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = true)
+    {
+        if (!@is_dir($this->_getNativePath($path))) {
+            if ($autocreate) {
+                $res = $this->autocreatePath($path);
+                if (is_a($res, 'PEAR_Error')) {
+                    return $res;
+                }
+            } else {
+                return PEAR::raiseError(_("VFS directory does not exist."));
+            }
+        }
+
+        // Treat an attempt to write an empty file as a touch() call,
+        // since otherwise the file will not be created at all.
+        if (!strlen($data)) {
+            if (@touch($this->_getNativePath($path, $name))) {
+                return true;
+            } else {
+                return PEAR::raiseError(_("Unable to create empty VFS file."));
+            }
+        }
+
+        $res = $this->_checkQuotaWrite('string', $data);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        // Otherwise we go ahead and try to write out the file.
+        if (function_exists('file_put_contents')) {
+            if (!@file_put_contents($this->_getNativePath($path, $name), $data)) {
+                return PEAR::raiseError(_("Unable to write VFS file data."));
+            }
+        } else {
+            $fp = @fopen($this->_getNativePath($path, $name), 'w');
+            if (!$fp) {
+                return PEAR::raiseError(_("Unable to open VFS file for writing."));
+            }
+
+            if (!@fwrite($fp, $data)) {
+                return PEAR::raiseError(_("Unable to write VFS file data."));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Delete a file from the VFS.
+     *
+     * @param string $path  The path to store the file in.
+     * @param string $name  The filename to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $res = $this->_checkQuotaDelete($path, $name);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!@unlink($this->_getNativePath($path, $name))) {
+            return PEAR::raiseError(_("Unable to delete VFS file."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Delete a folder from the VFS.
+     *
+     * @param string $path        The path to delete the folder from.
+     * @param string $name        The foldername to use.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        if ($recursive) {
+            $result = $this->emptyFolder($path . '/' . $name);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $list = $this->listFolder($path . '/' . $name);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+            if (count($list)) {
+                return PEAR::raiseError(sprintf(_("Unable to delete %s, the directory is not empty"),
+                                                $path . '/' . $name));
+            }
+        }
+
+        if (!@rmdir($this->_getNativePath($path, $name))) {
+            return PEAR::raiseError(_("Unable to delete VFS directory."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  The path to create the folder in.
+     * @param string $name  The foldername to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        if (!@mkdir($this->_getNativePath($path, $name))) {
+            return PEAR::raiseError(_("Unable to create VFS directory."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if a given pathname is a folder.
+     *
+     * @param string $path  The path to the folder.
+     * @param string $name  The file/folder name.
+     *
+     * @return boolean  True if it is a folder, false otherwise.
+     */
+    function isFolder($path, $name)
+    {
+        return @is_dir($this->_getNativePath($path, $name));
+    }
+
+    /**
+     * Changes permissions for an item in the VFS.
+     *
+     * @param string $path         The path of directory of the item.
+     * @param string $name         The name of the item.
+     * @param integer $permission  The octal value of the new permission.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $name, $permission)
+    {
+        if (!@chmod($this->_getNativePath($path, $name), $permission)) {
+            return PEAR::raiseError(sprintf(_("Unable to change permission for VFS file %s/%s."), $path, $name));
+        }
+
+        return true;
+    }
+
+    /**
+     * Return a list of the contents of a folder.
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success, PEAR_Error on error.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        $files = array();
+        $path = isset($path) ? $this->_getNativePath($path) : $this->_getNativePath();
+
+        if (!@is_dir($path)) {
+            return PEAR::raiseError(_("Not a directory"));
+        }
+
+        if (!@chdir($path)) {
+            return PEAR::raiseError(_("Unable to access VFS directory."));
+        }
+
+        $handle = opendir($path);
+        while (($entry = readdir($handle)) !== false) {
+            // Filter out '.' and '..' entries.
+            if ($entry == '.' || $entry == '..') {
+                continue;
+            }
+
+            // Filter out dotfiles if they aren't wanted.
+            if (!$dotfiles && substr($entry, 0, 1) == '.') {
+                continue;
+            }
+
+            // File name
+            $file['name'] = $entry;
+
+            // Unix style file permissions
+            $file['perms'] = $this->_getUnixPerms(fileperms($entry));
+
+            // Owner
+            $file['owner'] = fileowner($entry);
+            if (function_exists('posix_getpwuid')) {
+                $owner = posix_getpwuid($file['owner']);
+                $file['owner'] = $owner['name'];
+            }
+
+            // Group
+            $file['group'] = filegroup($entry);
+            if (function_exists('posix_getgrgid')) {
+                if (PHP_VERSION != '5.2.1') {
+                    $group = posix_getgrgid($file['group']);
+                    $file['group'] = $group['name'];
+                }
+            }
+
+            // Size
+            $file['size'] = filesize($entry);
+
+            // Date
+            $file['date'] = filemtime($entry);
+
+            // Type
+            if (@is_dir($entry) && !is_link($entry)) {
+                $file['perms'] = 'd' . $file['perms'];
+                $file['type'] = '**dir';
+                $file['size'] = -1;
+            } elseif (is_link($entry)) {
+                $file['perms'] = 'l' . $file['perms'];
+                $file['type'] = '**sym';
+                $file['link'] = readlink($entry);
+                $file['linktype'] = '**none';
+                if (file_exists($file['link'])) {
+                    if (is_dir($file['link'])) {
+                        $file['linktype'] = '**dir';
+                    } elseif (is_link($file['link'])) {
+                        $file['linktype'] = '**sym';
+                    } elseif (is_file($file['link'])) {
+                        $ext = explode('.', $file['link']);
+                        if (!(count($ext) == 1 || ($ext[0] === '' && count($ext) == 2))) {
+                            $file['linktype'] = VFS::strtolower($ext[count($ext) - 1]);
+                        }
+                    }
+                } else {
+                    $file['linktype'] = '**broken';
+                }
+            } elseif (is_file($entry)) {
+                $file['perms'] = '-' . $file['perms'];
+                $ext = explode('.', $entry);
+
+                if (count($ext) == 1 || (substr($file['name'], 0, 1) === '.' && count($ext) == 2)) {
+                    $file['type'] = '**none';
+                } else {
+                    $file['type'] = VFS::strtolower($ext[count($ext) - 1]);
+                }
+            } else {
+                $file['type'] = '**none';
+                if ((fileperms($entry) & 0xC000) == 0xC000) {
+                    $file['perms'] = 's' . $file['perms'];
+                } elseif ((fileperms($entry) & 0x6000) == 0x6000) {
+                    $file['perms'] = 'b' . $file['perms'];
+                } elseif ((fileperms($entry) & 0x2000) == 0x2000) {
+                    $file['perms'] = 'c' . $file['perms'];
+                } elseif ((fileperms($entry) & 0x1000) == 0x1000) {
+                    $file['perms'] = 'p' . $file['perms'];
+                } else {
+                    $file['perms'] = '?' . $file['perms'];
+                }
+            }
+
+            // Filtering.
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+        }
+
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $folders = array();
+        $folders[dirname($path)] = array('val' => dirname($path),
+                                         'abbrev' => '..',
+                                         'label' => '..');
+
+        $folderList = $this->listFolder($path, null, $dotfolders, true);
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        foreach ($folderList as $name => $files) {
+            $folders[$name] = array('val' => $path . '/' . $files['name'],
+                                    'abbrev' => $files['name'],
+                                    'label' => $path . '/' . $files['name']);
+        }
+
+        ksort($folders);
+
+        return $folders;
+    }
+
+    /**
+     * Return Unix style perms.
+     *
+     * @access private
+     *
+     * @param integer $perms  The permissions to set.
+     *
+     * @return string  Unix style perms.
+     */
+    function _getUnixPerms($perms)
+    {
+        // Determine permissions
+        $owner['read']    = ($perms & 00400) ? 'r' : '-';
+        $owner['write']   = ($perms & 00200) ? 'w' : '-';
+        $owner['execute'] = ($perms & 00100) ? 'x' : '-';
+        $group['read']    = ($perms & 00040) ? 'r' : '-';
+        $group['write']   = ($perms & 00020) ? 'w' : '-';
+        $group['execute'] = ($perms & 00010) ? 'x' : '-';
+        $world['read']    = ($perms & 00004) ? 'r' : '-';
+        $world['write']   = ($perms & 00002) ? 'w' : '-';
+        $world['execute'] = ($perms & 00001) ? 'x' : '-';
+
+        // Adjust for SUID, SGID and sticky bit
+        if ($perms & 0x800) {
+            $owner['execute'] = ($owner['execute'] == 'x') ? 's' : 'S';
+        }
+        if ($perms & 0x400) {
+            $group['execute'] = ($group['execute'] == 'x') ? 's' : 'S';
+        }
+        if ($perms & 0x200) {
+            $world['execute'] = ($world['execute'] == 'x') ? 't' : 'T';
+        }
+
+        $unixPerms = $owner['read'] . $owner['write'] . $owner['execute'] .
+                     $group['read'] . $group['write'] . $group['execute'] .
+                     $world['read'] . $world['write'] . $world['execute'];
+
+        return $unixPerms;
+    }
+
+    /**
+     * Rename a file or folder in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        if (!@is_dir($this->_getNativePath($newpath))) {
+            if (is_a($res = $this->autocreatePath($newpath), 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        if (!@rename($this->_getNativePath($oldpath, $oldname),
+                     $this->_getNativePath($newpath, $newname))) {
+            return PEAR::raiseError(sprintf(_("Unable to rename VFS file %s/%s."), $oldpath, $oldname));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns if a given file or folder exists in a folder.
+     *
+     * @param string $path  The path to the folder.
+     * @param string $name  The file or folder name.
+     *
+     * @return boolean  True if it exists, false otherwise.
+     */
+    function exists($path, $name)
+    {
+        return file_exists($this->_getNativePath($path, $name));
+    }
+
+    /**
+     * Return a full filename on the native filesystem, from a VFS
+     * path and name.
+     *
+     * @access private
+     *
+     * @param string $path  The VFS file path.
+     * @param string $name  The VFS filename.
+     *
+     * @return string  The full native filename.
+     */
+    function _getNativePath($path = '', $name = '')
+    {
+        $name = basename($name);
+        if (strlen($name)) {
+            $name = str_replace('..', '', $name);
+            if (substr($name, 0, 1) != '/') {
+                $name = '/' . $name;
+            }
+        }
+
+        if (strlen($path)) {
+            if (isset($this->_params['home']) &&
+                preg_match('|^~/?(.*)$|', $path, $matches)) {
+                $path = $this->_params['home'] . '/' . $matches[1];
+            }
+
+            $path = str_replace('..', '', $path);
+            if (substr($path, 0, 1) == '/') {
+                return $this->_params['vfsroot'] . $path . $name;
+            } else {
+                return $this->_params['vfsroot'] . '/' . $path . $name;
+            }
+        } else {
+            return $this->_params['vfsroot'] . $name;
+        }
+    }
+
+    /**
+     * Stub to check if we have a valid connection. Makes sure that
+     * the vfsroot is readable.
+     *
+     * @access private
+     *
+     * @return mixed  True if vfsroot is readable, PEAR_Error if it isn't.
+     */
+    function _connect()
+    {
+        if ((@is_dir($this->_params['vfsroot']) &&
+             is_readable($this->_params['vfsroot'])) ||
+            @mkdir($this->_params['vfsroot'])) {
+            return true;
+        } else {
+            return PEAR::raiseError(_("Unable to read the vfsroot directory."));
+        }
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/ftp.php b/framework/VFS/lib/VFS/ftp.php
new file mode 100644 (file)
index 0000000..2bb6934
--- /dev/null
@@ -0,0 +1,966 @@
+<?php
+/**
+ * VFS implementation for an FTP server.
+ *
+ * Required values for $params:<pre>
+ *      'username'       The username with which to connect to the ftp server.
+ *      'password'       The password with which to connect to the ftp server.
+ *      'hostspec'       The ftp server to connect to.</pre>
+ *
+ * Optional values for $params:<pre>
+ *      'lsformat'       The return formatting from the 'ls' command).
+ *                       Values: 'aix', 'standard' (default)
+ *      'maplocalids'    If true and the POSIX extension is available, the
+ *                       driver will map the user and group IDs returned from
+ *                       the FTP server with the local IDs from the local
+ *                       password file.  This is useful only if the FTP server
+ *                       is running on localhost or if the local user/group
+ *                       IDs are identical to the remote FTP server.
+ *      'pasv'           If true, connection will be set to passive mode.
+ *      'port'           The port used to connect to the ftp server if other
+ *                       than 21.
+ *      'ssl'            If true, and PHP had been compiled with OpenSSL
+ *                       support, TLS transport-level encryption will be
+ *                       negotiated with the server.
+ *      'timeout'        If defined, use this value as the timeout for the
+ *                       server.
+ *      'type'           The type of the remote FTP server.
+ *                       Possible values: 'unix', 'win', 'netware'
+ *                       By default, we attempt to auto-detect type.</pre>
+ *
+ * $Horde: framework/VFS/lib/VFS/ftp.php,v 1.7 2009/10/15 17:13:28 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ * Copyright 2002-2007 Michael Varghese <mike.varghese@ascellatech.com>
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael Varghese <mike.varghese@ascellatech.com>
+ * @package VFS
+ */
+class VFS_ftp extends VFS {
+
+    /**
+     * List of additional credentials required for this VFS backend.
+     *
+     * @var array
+     */
+    var $_credentials = array('username', 'password');
+
+    /**
+     * List of permissions and if they can be changed in this VFS backend.
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => true, 'write' => true, 'execute' => true),
+        'group' => array('read' => true, 'write' => true, 'execute' => true),
+        'all'   => array('read' => true, 'write' => true, 'execute' => true));
+
+    /**
+     * Variable holding the connection to the ftp server.
+     *
+     * @var resource
+     */
+    var $_stream = false;
+
+    /**
+     * Local cache array for user IDs.
+     *
+     * @var array
+     */
+    var $_uids = array();
+
+    /**
+     * Local cache array for group IDs.
+     *
+     * @var array
+     */
+    var $_gids = array();
+
+    /**
+     */
+    var $_type;
+
+    /**
+     * Returns the size of a file.
+     *
+     * @access public
+     *
+     * @param string $path  The path of the file.
+     * @param string $name  The filename.
+     *
+     * @return integer  The size of the file in bytes or PEAR_Error on
+     *                  failure.
+     */
+    function size($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (($size = @ftp_size($this->_stream, $this->_getPath($path, $name))) === false) {
+            return PEAR::raiseError(sprintf(_("Unable to check file size of \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return $size;
+    }
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        $size = filesize($file);
+        if ($size === 0) {
+            return '';
+        }
+
+        return file_get_contents($file);
+    }
+
+    /**
+     * Retrieves a file from the VFS as an on-disk local file.
+     *
+     * This function provides a file on local disk with the data of a VFS file
+     * in it. This file <em>cannot</em> be modified! The behavior if you do
+     * modify it is undefined. It will be removed at the end of the request.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string A local filename.
+     */
+    function readFile($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        // Create a temporary file and register it for deletion at the
+        // end of this request.
+        $localFile = $this->_getTempFile();
+        if (!$localFile) {
+            return PEAR::raiseError(_("Unable to create temporary file."));
+        }
+        register_shutdown_function(create_function('', '@unlink(\'' . addslashes($localFile) . '\');'));
+
+        $result = @ftp_get(
+            $this->_stream,
+            $localFile,
+            $this->_getPath($path, $name),
+            FTP_BINARY);
+        if ($result === false) {
+            return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return $localFile;
+    }
+
+    /**
+     * Open a stream to a file in the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return resource  The stream.
+     */
+    function readStream($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        $mode = OS_WINDOWS ? 'rb' : 'r';
+        return fopen($file, $mode);
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $res = $this->_checkQuotaWrite('file', $tmpFile);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!@ftp_put($this->_stream, $this->_getPath($path, $name), $tmpFile, FTP_BINARY)) {
+            if ($autocreate) {
+                $result = $this->autocreatePath($path);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                if (!@ftp_put($this->_stream, $this->_getPath($path, $name), $tmpFile, FTP_BINARY)) {
+                    return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
+                }
+            } else {
+                return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $res = $this->_checkQuotaWrite('string', $data);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $tmpFile = $this->_getTempFile();
+        $fp = fopen($tmpFile, 'wb');
+        fwrite($fp, $data);
+        fclose($fp);
+
+        $result = $this->write($path, $name, $tmpFile, $autocreate);
+        unlink($tmpFile);
+        return $result;
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $res = $this->_checkQuotaDelete($path, $name);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if a given item is a folder.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The item name.
+     *
+     * @return boolean  True if it is a folder, false otherwise.
+     */
+    function isFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $result = false;
+        $olddir = $this->getCurrentDirectory();
+
+        /* See if we can change to the given path. */
+        if (@ftp_chdir($this->_stream, $this->_getPath($path, $name))) {
+            $result = true;
+        }
+
+        $this->_setPath($olddir);
+
+        return $result;
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path        The parent folder.
+     * @param string $name        The name of the folder to delete.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $isDir = false;
+        $dirCheck = $this->listFolder($path);
+        foreach ($dirCheck as $file) {
+            if ($file['name'] == $name && $file['type'] == '**dir') {
+                $isDir = true;
+                break;
+            }
+        }
+
+        if ($isDir) {
+            $file_list = $this->listFolder($this->_getPath($path, $name));
+            if (is_a($file_list, 'PEAR_Error')) {
+                return $file_list;
+            }
+
+            if (count($file_list) && !$recursive) {
+                return PEAR::raiseError(sprintf(_("Unable to delete \"%s\", the directory is not empty."),
+                                                $this->_getPath($path, $name)));
+            }
+
+            foreach ($file_list as $file) {
+                if ($file['type'] == '**dir') {
+                    $result = $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
+                } else {
+                    $result = $this->deleteFile($this->_getPath($path, $name), $file['name']);
+                }
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            if (!@ftp_rmdir($this->_stream, $this->_getPath($path, $name))) {
+                return PEAR::raiseError(sprintf(_("Cannot remove directory \"%s\"."), $this->_getPath($path, $name)));
+            }
+        } else {
+            if (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
+                return PEAR::raiseError(sprintf(_("Cannot delete file \"%s\"."), $this->_getPath($path, $name)));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Renames a file in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        if (is_a($conn = $this->_connect(), 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!@ftp_rename($this->_stream, $this->_getPath($oldpath, $oldname), $this->_getPath($newpath, $newname))) {
+            return PEAR::raiseError(sprintf(_("Unable to rename VFS file \"%s\"."), $this->_getPath($oldpath, $oldname)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ftp_mkdir($this->_stream, $this->_getPath($path, $name))) {
+            return PEAR::raiseError(sprintf(_("Unable to create VFS directory \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Changes permissions for an item on the VFS.
+     *
+     * @param string $path        The parent folder of the item.
+     * @param string $name        The name of the item.
+     * @param string $permission  The permission to set.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $name, $permission)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ftp_site($this->_stream, 'CHMOD ' . $permission . ' ' . $this->_getPath($path, $name))) {
+            return PEAR::raiseError(sprintf(_("Unable to change permission for VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns an an unsorted file list of the specified directory.
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function _listFolder($path = '', $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $files = array();
+
+        if (empty($this->_type)) {
+            if (!empty($this->_params['type'])) {
+                $this->_type = $this->_params['type'];
+            } else {
+                $type = VFS::strtolower(@ftp_systype($this->_stream));
+                if ($type == 'unknown') {
+                    // Go with unix-style listings by default.
+                    $type = 'unix';
+                } elseif (strpos($type, 'win') !== false) {
+                    $type = 'win';
+                } elseif (strpos($type, 'netware') !== false) {
+                    $type = 'netware';
+                }
+
+                $this->_type = $type;
+            }
+        }
+
+        $olddir = $this->getCurrentDirectory();
+        if (strlen($path)) {
+            $res = $this->_setPath($path);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        if ($this->_type == 'unix') {
+            // If we don't want dotfiles, We can save work here by not
+            // doing an ls -a and then not doing the check later (by
+            // setting $dotfiles to true, the if is short-circuited).
+            if ($dotfiles) {
+                $list = ftp_rawlist($this->_stream, '-al');
+                $dotfiles = true;
+            } else {
+                $list = ftp_rawlist($this->_stream, '-l');
+            }
+        } else {
+           $list = ftp_rawlist($this->_stream, '');
+        }
+
+        if (!is_array($list)) {
+            if (isset($olddir)) {
+                $res = $this->_setPath($olddir);
+                if (is_a($res, 'PEAR_Error')) {
+                    return $res;
+                }
+            }
+            return array();
+        }
+
+        /* If 'maplocalids' is set, check for the POSIX extension. */
+        $mapids = false;
+        if (!empty($this->_params['maplocalids']) &&
+            extension_loaded('posix')) {
+            $mapids = true;
+        }
+
+        $currtime = time();
+
+        foreach ($list as $line) {
+            $file = array();
+            $item = preg_split('/\s+/', $line);
+            if (($this->_type == 'unix') ||
+                (($this->_type == 'win') && !preg_match('|\d\d-\d\d-\d\d|', $item[0]))) {
+                if (count($item) < 8 || substr($line, 0, 5) == 'total') {
+                    continue;
+                }
+                $file['perms'] = $item[0];
+                if ($mapids) {
+                    if (!isset($this->_uids[$item[2]])) {
+                        $entry = posix_getpwuid($item[2]);
+                        $this->_uids[$item[2]] = (empty($entry)) ? $item[2] : $entry['name'];
+                    }
+                    $file['owner'] = $this->_uids[$item[2]];
+                    if (!isset($this->_uids[$item[3]])) {
+                        $entry = posix_getgrgid($item[3]);
+                        $this->_uids[$item[3]] = (empty($entry)) ? $item[3] : $entry['name'];
+                    }
+                    $file['group'] = $this->_uids[$item[3]];
+
+                } else {
+                    $file['owner'] = $item[2];
+                    $file['group'] = $item[3];
+                }
+
+                if (!empty($this->_params['lsformat']) &&
+                    ($this->_params['lsformat'] == 'aix')) {
+                    $file['name'] = substr($line, strpos($line, sprintf("%s %2s %-5s", $item[5], $item[6], $item[7])) + 13);
+                } else {
+                    $file['name'] = substr($line, strpos($line, sprintf("%s %2s %5s", $item[5], $item[6], $item[7])) + 13);
+                }
+
+                // Filter out '.' and '..' entries.
+                if (preg_match('/^\.\.?\/?$/', $file['name'])) {
+                    continue;
+                }
+
+                // Filter out dotfiles if they aren't wanted.
+                if (!$dotfiles && substr($file['name'], 0, 1) == '.') {
+                    continue;
+                }
+
+                $p1 = substr($file['perms'], 0, 1);
+                if ($p1 === 'l') {
+                    $file['link'] = substr($file['name'], strpos($file['name'], '->') + 3);
+                    $file['name'] = substr($file['name'], 0, strpos($file['name'], '->') - 1);
+                    $file['type'] = '**sym';
+
+                   if ($this->isFolder('', $file['link'])) {
+                              $file['linktype'] = '**dir';
+                                                    } else {
+                                                    $parts = explode('/', $file['link']);
+                                                    $name = explode('.', array_pop($parts));
+                                                    if (count($name) == 1 || ($name[0] === '' && count($name) == 2)) {
+                                                        $file['linktype'] = '**none';
+                                                        } else {
+                                                            $file['linktype'] = VFS::strtolower(array_pop($name));
+                                                            }
+                                                                   }
+                } elseif ($p1 === 'd') {
+                    $file['type'] = '**dir';
+                } else {
+                    $name = explode('.', $file['name']);
+                    if (count($name) == 1 || (substr($file['name'], 0, 1) === '.' && count($name) == 2)) {
+                        $file['type'] = '**none';
+                    } else {
+                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                    }
+                }
+                if ($file['type'] == '**dir') {
+                    $file['size'] = -1;
+                } else {
+                    $file['size'] = $item[4];
+                }
+                if (strpos($item[7], ':') !== false) {
+                    $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . date('Y', $currtime));
+                    // If the ftp server reports a file modification date more
+                    // less than one day in the future, don't try to subtract
+                    // a year from the date.  There is no way to know, for
+                    // example, if the VFS server and the ftp server reside
+                    // in different timezones.  We should simply report to the
+                    //  user what the FTP server is returning.
+                    if ($file['date'] > ($currtime + 86400)) {
+                        $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . (date('Y', $currtime) - 1));
+                    }
+                } else {
+                    $file['date'] = strtotime('00:00:00' . $item[5] . ' ' . $item[6] . ' ' . $item[7]);
+                }
+            } elseif ($this->_type == 'netware') {
+                if (count($item) < 8 || substr($line, 0, 5) == 'total') {
+                    continue;
+                }
+
+                $file = array();
+                $file['perms'] = $item[1];
+                $file['owner'] = $item[2];
+                if ($item[0] == 'd') {
+                    $file['type'] = '**dir';
+                } else {
+                    $file['type'] = '**none';
+                }
+                $file['size'] = $item[3];
+
+                // We don't know the timezone here. Just report what the FTP server says.
+                if (strpos($item[6], ':') !== false) {
+                    $file['date'] = strtotime($item[6] . ':00 ' . $item[5] . ' ' . $item[4] . ' ' . date('Y'));
+                } else {
+                    $file['date'] = strtotime('00:00:00 ' . $item[5] . ' ' . $item[4] . ' ' . $item[6]);
+                }
+
+                $file['name'] = $item[7];
+                $index = 8;
+                while ($index < count($item)) {
+                    $file['name'] .= ' ' . $item[$index];
+                    $index++;
+                }
+            } else {
+                /* Handle Windows FTP servers returning DOS-style file
+                 * listings. */
+                $file['perms'] = '';
+                $file['owner'] = '';
+                $file['group'] = '';
+                $file['name'] = $item[3];
+                $index = 4;
+                while ($index < count($item)) {
+                    $file['name'] .= ' ' . $item[$index];
+                    $index++;
+                }
+                $file['date'] = strtotime($item[0] . ' ' . $item[1]);
+                if ($item[2] == '<DIR>') {
+                    $file['type'] = '**dir';
+                    $file['size'] = -1;
+                } else {
+                    $file['size'] = $item[2];
+                    $name = explode('.', $file['name']);
+                    if (count($name) == 1 || (substr($file['name'], 0, 1) === '.' && count($name) == 2)) {
+                        $file['type'] = '**none';
+                    } else {
+                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                    }
+                }
+            }
+
+            // Filtering.
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+        }
+
+        if (isset($olddir)) {
+            $res = $this->_setPath($olddir);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in the specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $folders = array();
+        $folder = array();
+
+        $folderList = $this->listFolder($path, null, $dotfolders, true);
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        $folder['val'] = $this->_parentDir($path);
+        $folder['abbrev'] = '..';
+        $folder['label'] = '..';
+
+        $folders[$folder['val']] = $folder;
+
+        foreach ($folderList as $files) {
+            $folder['val'] = $this->_getPath($path, $files['name']);
+            $folder['abbrev'] = $files['name'];
+            $folder['label'] = $folder['val'];
+
+            $folders[$folder['val']] = $folder;
+        }
+
+        ksort($folders);
+        return $folders;
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The name of the destination directory.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
+            }
+        }
+
+        if ($this->isFolder($path, $name)) {
+            if (is_a($result = $this->_copyRecursive($path, $name, $dest), 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $tmpFile = $this->_getTempFile();
+            $fetch = @ftp_get($this->_stream, $tmpFile, $orig, FTP_BINARY);
+            if (!$fetch) {
+                unlink($tmpFile);
+                return PEAR::raiseError(sprintf(_("Failed to copy from \"%s\"."), $orig));
+            }
+
+            $res = $this->_checkQuotaWrite('file', $tmpFile);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+
+            if (!@ftp_put($this->_stream, $this->_getPath($dest, $name), $tmpFile, FTP_BINARY)) {
+                unlink($tmpFile);
+                return PEAR::raiseError(sprintf(_("Failed to copy to \"%s\"."), $this->_getPath($dest, $name)));
+            }
+
+            unlink($tmpFile);
+        }
+
+        return true;
+    }
+
+    /**
+     * Moves a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The destination file name.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
+            }
+        }
+
+        if (!@ftp_rename($this->_stream, $orig, $this->_getPath($dest, $name))) {
+            return PEAR::raiseError(sprintf(_("Failed to move to \"%s\"."), $this->_getPath($dest, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the current working directory on the FTP server.
+     *
+     * @return string  The current working directory.
+     */
+    function getCurrentDirectory()
+    {
+        if (is_a($connected = $this->_connect(), 'PEAR_Error')) {
+            return $connected;
+        }
+        return @ftp_pwd($this->_stream);
+    }
+
+    /**
+     * Changes the current directory on the server.
+     *
+     * @access private
+     *
+     * @param string $path  The path to change to.
+     *
+     * @return mixed  True on success, or a PEAR_Error on failure.
+     */
+    function _setPath($path)
+    {
+        if (!@ftp_chdir($this->_stream, $path)) {
+            return PEAR::raiseError(sprintf(_("Unable to change to %s."), $path));
+        }
+        return true;
+    }
+
+    /**
+     * Returns the parent directory of the specified path.
+     *
+     * @access private
+     *
+     * @param string $path  The path to get the parent of.
+     *
+     * @return string  The parent directory (string) on success or a PEAR_Error
+     *                 object on failure.
+     */
+    function _parentDir($path)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $olddir = $this->getCurrentDirectory();
+        @ftp_cdup($this->_stream);
+
+        $parent = $this->getCurrentDirectory();
+        $this->_setPath($olddir);
+
+        if (!$parent) {
+            return PEAR::raiseError(_("Unable to determine current directory."));
+        }
+
+        return $parent;
+    }
+
+    /**
+     * Attempts to open a connection to the FTP server.
+     *
+     * @access private
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _connect()
+    {
+        if ($this->_stream === false) {
+            if (!extension_loaded('ftp')) {
+                return PEAR::raiseError(_("The FTP extension is not available."));
+            }
+
+            if (!is_array($this->_params)) {
+                return PEAR::raiseError(_("No configuration information specified for FTP VFS."));
+            }
+
+            $required = array('hostspec', 'username', 'password');
+            foreach ($required as $val) {
+                if (!isset($this->_params[$val])) {
+                    return PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), $val));
+                }
+            }
+
+            /* Connect to the ftp server using the supplied parameters. */
+            if (!empty($this->_params['ssl'])) {
+                if (function_exists('ftp_ssl_connect')) {
+                    $this->_stream = @ftp_ssl_connect($this->_params['hostspec'], $this->_params['port']);
+                } else {
+                    return PEAR::raiseError(_("Unable to connect with SSL."));
+                }
+            } else {
+                $this->_stream = @ftp_connect($this->_params['hostspec'], $this->_params['port']);
+            }
+            if (!$this->_stream) {
+                return PEAR::raiseError(_("Connection to FTP server failed."));
+            }
+
+            $connected = @ftp_login($this->_stream, $this->_params['username'], $this->_params['password']);
+            if (!$connected) {
+                @ftp_quit($this->_stream);
+                $this->_stream = false;
+                return PEAR::raiseError(_("Authentication to FTP server failed."));
+            }
+
+            if (!empty($this->_params['pasv'])) {
+                @ftp_pasv($this->_stream, true);
+            }
+
+            if (!empty($this->_params['timeout'])) {
+                ftp_set_option($this->_stream, FTP_TIMEOUT_SEC, $this->_params['timeout']);
+            }
+        }
+
+        return true;
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/horde.php b/framework/VFS/lib/VFS/horde.php
new file mode 100644 (file)
index 0000000..65015e9
--- /dev/null
@@ -0,0 +1,335 @@
+<?php
+/**
+ * VFS implementation for the Horde Application Framework.
+ *
+ * Required parameters:<pre>
+ *   'horde_base'  Filesystem location of a local Horde installation.</pre>
+ *
+ * Optional parameters:<pre>
+ *   'user'      A valid Horde user name.
+ *   'password'  The user's password.</pre>
+ *
+ * $Horde: framework/VFS/lib/VFS/horde.php,v 1.8 2009-11-22 18:15:23 slusarz Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.2
+ * @package VFS
+ */
+class VFS_horde extends VFS {
+
+    /**
+     * Reference to a Horde Registry instance.
+     *
+     * @var Registry
+     */
+    var $_registry;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  A hash containing connection parameters.
+     */
+    function VFS_horde($params = array())
+    {
+        parent::VFS($params);
+        if (!isset($this->_params['horde_base'])) {
+            $this->_registry = PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), 'horde_base'));
+            return;
+        }
+
+        // Define path to Horde.
+        @define('HORDE_BASE', $this->_params['horde_base']);
+
+        // Load the Horde Framework core, and set up inclusion paths.
+        require_once HORDE_BASE . '/lib/core.php';
+
+        // Create the Registry object.
+        $this->_registry = Horde_Registry::singleton();
+    }
+
+    function _connect()
+    {
+        if (!empty($this->_params['user']) &&
+            !empty($this->_params['password'])) {
+            Horde_Auth::setAuth($this->_params['user'],
+                           array('password' => $this->_params['password']));
+        }
+    }
+
+    /**
+     * Retrieves the size of a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return integer The file size.
+     */
+    function size($path, $name)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string The file data.
+     */
+    function read($path, $name)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+        $pieces = explode('/', $path);
+
+        try {
+            $data = $this->_registry->callByPackage($pieces[0], 'browse', array('path' => $path . '/' . $name));
+        } catch (Horde_Exception $e) {
+            return '';
+        }
+
+        return is_object($data) ? $data : $data['data'];
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @abstract
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Moves a file through the backend.
+     *
+     * @abstract
+     *
+     * @param string $path  The path of the original file.
+     * @param string $name  The name of the original file.
+     * @param string $dest  The destination file name.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @abstract
+     *
+     * @param string $path  The path of the original file.
+     * @param string $name  The name of the original file.
+     * @param string $dest  The name of the destination directory.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Renames a file in the VFS.
+     *
+     * @abstract
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Returns an an unsorted file list of the specified directory.
+     *
+     * @abstract
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        $list = array();
+        if ($path == '/') {
+            $apps = $this->_registry->listApps(null, false, Horde_Perms::READ);
+            if (is_a($apps, 'PEAR_Error')) {
+                return $apps;
+            }
+            foreach ($apps as $app) {
+                if ($this->_registry->hasMethod('browse', $app)) {
+                    $file = array(
+                        //'name' => $this->_registry->get('name', $app),
+                        'name' => $app,
+                        'date' => time(),
+                        'type' => '**dir',
+                        'size' => -1
+                    );
+                    $list[] = $file;
+                }
+            }
+            return $list;
+        }
+
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+        $pieces = explode('/', $path);
+
+        try {
+            $items = $this->_registry->callByPackage($pieces[0], 'browse', array('path' => $path, 'properties' => array('name', 'browseable', 'contenttype', 'contentlength', 'modified')));
+        } catch (Horde_Exception $e) {
+            return PEAR::raiserError($e->getMessage(), $e->getCode());
+        }
+
+        if (!is_array(reset($items))) {
+            /* We return an object's content. */
+            return PEAR::raiseError(_("unknown error"));
+        }
+
+        include_once 'Horde/MIME/Magic.php';
+        foreach ($items as $sub_path => $i) {
+            if ($dironly && !$i['browseable']) {
+                continue;
+            }
+
+            $name = basename($sub_path);
+            if ($this->_filterMatch($filter, $name)) {
+                continue;
+            }
+
+            if (class_exists('MIME_Magic')) {
+                $type = empty($i['contenttype']) ? 'application/octet-stream' : $i['contenttype'];
+                $type = MIME_Magic::MIMEToExt($type);
+            } else {
+                $type = '**none';
+            }
+
+            $file = array(
+                //'name' => $i['name'],
+                'name' => $name,
+                'date' => empty($i['modified']) ? 0 : $i['modified'],
+                'type' => $i['browseable'] ? '**dir' : $type,
+                'size' => empty($i['contentlength']) ? 0 : $i['contentlength']
+            );
+            $list[] = $file;
+        }
+
+        return $list;
+    }
+
+    /**
+     * Returns a sorted list of folders in the specified directory.
+     *
+     * @abstract
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        if (is_a($this->_registry, 'PEAR_Error')) {
+            return $this->_registry;
+        }
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/kolab.php b/framework/VFS/lib/VFS/kolab.php
new file mode 100644 (file)
index 0000000..3aebdc1
--- /dev/null
@@ -0,0 +1,636 @@
+<?php
+
+/** We need the Kolab Storage library for accessing the server. */
+require_once 'Horde/Kolab/Storage/List.php';
+
+/**
+ * VFS implementation for a Kolab IMAP server.
+ *
+ * $Horde: framework/VFS/lib/VFS/kolab.php,v 1.5 2009/07/08 18:39:08 slusarz Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package VFS
+ */
+class VFS_kolab extends VFS {
+
+    /**
+     * Variable holding the connection to the Kolab storage system.
+     *
+     * @var Horde_Kolab_IMAP
+     */
+    var $_imap = false;
+
+    /**
+     * Cache for the list of folders.
+     *
+     * @var array
+     */
+    var $_folders;
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        list($app, $uid) = $this->_getAppUid($path);
+        if ($app && $uid) {
+            $handler = &$this->_getAppHandler($app, $uid);
+            if (is_a($handler, 'PEAR_Error')) {
+                return $handler;
+            }
+            $object = $handler->getObject($uid);
+
+            if (isset($object['_attachments'][$name])) {
+                return $handler->getAttachment($object['_attachments'][$name]['key']);
+            }
+        }
+
+        //FIXME
+        if ($this->isFolder(dirname($path), basename($path))) {
+            $session = &Horde_Kolab_Session::singleton();
+            $imap = &$session->getImap();
+
+            $result = $imap->select(substr($path,1));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $file = explode('/', $name);
+
+            return $this->_getFile($imap, $file[0], $file[1]);
+        }
+        return '';
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        list($app, $uid) = $this->_getAppUid($path);
+        if ($app) {
+            $handler = &$this->_getAppHandler($app, $uid);
+            if (is_a($handler, 'PEAR_Error')) {
+                return $handler;
+            }
+            $object = $handler->getObject($uid);
+            $object['_attachments'][$name]['path'] = $tmpFile;
+            if (empty($object['link-attachment'])) {
+                $object['link-attachment'] = array($name);
+            } else {
+                $object['link-attachment'][] = $name;
+            }
+
+            return $handler->save($object, $uid);
+        }
+
+        if ($autocreate && !$this->isFolder(dirname($path), basename($path))) {
+            $result = $this->autocreatePath($path);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        //FIXME
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @abstract
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        list($app, $uid) = $this->_getAppUid($path);
+        if ($app) {
+            $handler = &$this->_getAppHandler($app, $uid);
+            if (is_a($handler, 'PEAR_Error')) {
+                return $handler;
+            }
+            $object = $handler->getObject($uid);
+            if (!isset($object['_attachments'][$name])) {
+                return PEAR::raiseError(_("Unable to delete VFS file."));
+            }
+            unset($object['_attachments'][$name]);
+            $object['link-attachment'] = array_values(array_diff($object['link-attachment'], array($name)));
+
+            return $handler->save($object, $uid);
+        }
+
+        //FIXME
+        return PEAR::raiseError(_("Not supported."));
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $list = Kolab_List::singleton();
+        $folder = $this->_getFolder($path, $name);
+
+        $object = $list->getNewFolder();
+        $object->setName($folder);
+
+        $result = $object->save(array('type' => 'h-file'));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_folders = null;
+    }
+
+     /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path        The parent folder.
+     * @param string $name        The name of the folder to delete.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        if ($recursive) {
+            $result = $this->emptyFolder($path . '/' . $name);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $list = $this->listFolder($path . '/' . $name, null, false);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+            if (count($list)) {
+                return PEAR::raiseError(sprintf(_("Unable to delete %s, the directory is not empty"),
+                                                $path . '/' . $name));
+            }
+        }
+
+        list($app, $uid) = $this->_getAppUid($path . '/' . $name);
+        if ($app) {
+            /**
+             * Objects provide no real folders and we don't delete them.
+             */
+            return true;
+        }
+
+        $folders = $this->_getFolders();
+        if (is_a($folders, 'PEAR_Error')) {
+            return $folders;
+        }
+        $folder = $this->_getFolder($path, $name);
+
+        if (!empty($folders['/' . $folder])) {
+            $result = $folders['/' . $folder]->delete();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $this->_folders = null;
+
+            return true;
+        }
+        return PEAR::raiseError(sprintf('No such folder %s!', '/' . $folder));
+    }
+
+    /**
+     * Recursively remove all files and subfolders from the given
+     * folder.
+     *
+     * @param string $path  The path of the folder to empty.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function emptyFolder($path)
+    {
+        // Get and delete the subfolders.
+        $list = $this->listFolder($path, null, false, true);
+        if (is_a($list, 'PEAR_Error')) {
+            return $list;
+        }
+        foreach ($list as $folder) {
+            $result = $this->deleteFolder($path, $folder['name'], true);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        // Only files are left, get and delete them.
+        $list = $this->listFolder($path, null, false);
+        if (is_a($list, 'PEAR_Error')) {
+            return $list;
+        }
+        foreach ($list as $file) {
+            $result = $this->deleteFile($path, $file['name']);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns an an unsorted file list of the specified directory.
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function _listFolder($path = '', $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        list($app, $uid) = $this->_getAppUid($path);
+        if ($app) {
+            if ($dironly) {
+                /** 
+                 * Objects dont support directories.
+                 */
+                return array();
+            }
+            if ($uid) {
+                $handler = &$this->_getAppHandler($app, $uid);
+                if (is_a($handler, 'PEAR_Error')) {
+                    return $handler;
+                }
+                $object = $handler->getObject($uid);
+                if (is_a($object, 'PEAR_Error')) {
+                    return $object;
+                }
+
+                $filenames = isset($object['_attachments']) ? array_keys($object['_attachments']) : array();
+            } else {
+                $filenames = $this->_getAppUids($app);
+            }
+
+            $owner = Horde_Auth::getAuth();
+
+            $files = array();
+            $file = array();
+            foreach($filenames as $filename) {
+                $name = explode('.', $filename);
+
+                if (count($name) == 1) {
+                    $file['type'] = '**none';
+                } else {
+                    $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                }
+
+                $file['size'] = '-1';
+                $file['name'] = $filename;
+                $file['group'] = 'none';
+                $file['owner'] = $owner;
+                $file['date'] = 0;
+                $file['perms'] = 'rwxrwx---';
+
+                $files[$file['name']] = $file;
+            }
+            return $files;
+        }
+
+        $owner = Horde_Auth::getAuth();
+
+        $files = array();
+
+        $folders = $this->listFolders($path, $filter, $dotfiles);
+        if (is_a($folders, 'PEAR_Error')) {
+            return $folders;
+        }
+
+        $list = $this->_getFolders();
+
+        $file = array();
+        foreach ($folders as $folder) {
+            $file['type'] = '**dir';
+            $file['size'] = -1;
+            $file['name'] = $folder['abbrev'];
+            //FIXME
+            $file['group'] = 'none';
+            //FIXME
+            $file['owner'] = $owner;
+            //FIXME
+            $file['date'] = 0;
+            //FIXME
+            $file['perms'] = 'rwxrwx---';
+
+            $files[$file['name']] = $file;
+        }
+
+        if (!$dironly
+            && $this->isFolder(basename($path), basename($path))
+            && !empty($list[$path])) {
+
+            $session = &Horde_Kolab_Session::singleton();
+            $imap = &$session->getImap();
+
+            $result = $imap->select(substr($path, 1));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $uids = $imap->getUids();
+            if (is_a($uids, 'PEAR_Error')) {
+                return $uids;
+            }
+
+            foreach ($uids as $uid) {
+                $mFiles = $this->_parseMessage($imap, $uid);
+                if (is_a($mFiles, 'PEAR_Error')) {
+                    return $mFiles;
+                }
+                $result = array_merge($files, $mFiles);
+                $files = $result;
+            }
+        }
+
+        return $files;
+    }
+
+    function _parseMessage($imap, $uid)
+    {
+        $result = $imap->getMessageHeader($uid);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $raw_headers = $result;
+
+        $body = $imap->getMessageBody($uid);
+        if (is_a($body, 'PEAR_Error')) {
+            return $body;
+        }
+
+        $raw_message = $raw_headers . $body;
+
+        $mime_message = &MIME_Structure::parseTextMIMEMessage($raw_message);
+        $parts = $mime_message->contentTypeMap();
+
+        $owner = Horde_Auth::getAuth();
+
+        $files = array();
+        $file = array();
+
+        foreach ($parts as $part_id => $disposition) {
+            $part = $mime_message->getPart($part_id);
+
+            $filename = $part->getDispositionParameter('filename');
+
+            if ($filename) {
+                $file['type'] = '**file';
+                $file['size'] = $part->getSize();
+                $file['name'] = $uid . '/' . $filename;
+                //FIXME
+                $file['group'] = 'none';
+                //FIXME
+                $file['owner'] = $owner;
+                //FIXME
+                $file['date'] = 0;
+                //FIXME
+                $file['perms'] = 'rwxrwx---';
+
+                $files[$file['name']] = $file;
+            }
+
+        }
+
+        return $files;
+    }
+
+
+    function _getFile($imap, $uid, $filename)
+    {
+        $result = $imap->getMessageHeader($uid);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $raw_headers = $result;
+
+        $body = $imap->getMessageBody($uid);
+        if (is_a($body, 'PEAR_Error')) {
+            return $body;
+        }
+
+        $raw_message = $raw_headers . $body;
+
+        $mime_message = &MIME_Structure::parseTextMIMEMessage($raw_message);
+        $parts = $mime_message->contentTypeMap();
+
+        $owner = Horde_Auth::getAuth();
+
+        $files = array();
+        $file = array();
+
+        foreach ($parts as $part_id => $disposition) {
+            $part = $mime_message->getPart($part_id);
+
+            $f= $part->getDispositionParameter('filename');
+
+            if ($f && $f == $filename ) {
+                return $part->transferDecode();
+            }
+        }
+        return '';
+    }
+
+
+    /**
+     * Returns a sorted list of folders in the specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        if (substr($path, -1) != '/') {
+            $path .= '/';
+        }
+
+        $aFolders = array();
+        $aFolder = array();
+
+        if ($dotfolders && $path != '/') {
+            $aFolder['val'] = dirname($path);
+            $aFolder['abbrev'] = '..';
+            $aFolder['label'] = '..';
+
+            $aFolders[$aFolder['val']] = $aFolder;
+        }
+
+        $folders = $this->_getFolders();
+
+        $base_len = strlen($path);
+        foreach (array_keys($folders) as $folder) {
+            if (substr($folder, 0, $base_len) == $path) {
+                $name = substr($folder, $base_len);
+                if (!strpos($name, '/')) {
+                    $aFolder['val']    = $folder;
+                    $aFolder['abbrev'] = $name;
+                    $aFolder['label'] = $folder;
+                    $aFolders[$aFolder['val']] = $aFolder;
+                }
+            }
+        }
+
+        ksort($aFolders);
+        return $aFolders;
+    }
+
+    function _getFolder($path, $name)
+    {
+        $folder = $path . '/' . $name;
+
+        while (substr($folder, 0, 1) == '/') {
+            $folder = substr($folder, 1);
+        }
+
+        while (substr($folder, -1) == '/') {
+            $folder = substr($folder, 0, -1);
+        }
+
+        return $folder;
+    }
+
+
+    function _getFolders()
+    {
+        if (!isset($this->_folders)) {
+
+            $vfs_folders = array();
+
+            $list = Kolab_List::singleton();
+
+            if (!empty($this->_params['all_folders'])) {
+                $folders = $list->getFolders();
+            } else {
+                $folders = $list->getByType('h-file');
+            }
+
+            if (is_a($folders, 'PEAR_Error')) {
+                return $folders;
+            }
+
+            foreach ($folders as $folder) {
+                $vfs_folders['/' . $folder->name] = &$folder;
+            }
+
+            foreach (array_keys($vfs_folders) as $name) {
+                $dir = dirname($name);
+                while ($dir != '/') {
+                    if (!isset($vfs_folders[$dir])) {
+                        $vfs_folders[$dir] = null;
+                    }
+                    $dir = dirname($dir);
+                }
+            }
+            $this->_folders = $vfs_folders;
+        }
+        return $this->_folders;
+    }
+
+    function _getAppUid($path)
+    {
+        if (defined('TURBA_VFS_PATH')
+            && substr($path, 0, strlen(TURBA_VFS_PATH)) == TURBA_VFS_PATH) {
+            return array('turba', substr($path, strlen(TURBA_VFS_PATH) + 1));
+        }
+        return array(false, false);
+    }
+
+    function &_getAppHandler($app, $uid)
+    {
+        global $registry;
+
+        switch ($app) {
+        case 'turba':
+            $sources = $registry->call('contacts/sources',
+                                       array('writeable' => true));
+            $fields = array();
+            foreach (array_keys($sources) as $source) {
+                $fields[$source] = array('__uid');
+            }
+            $result = $registry->call('contacts/search',
+                                      array('names' => $uid,
+                                            'sources' => array_keys($sources),
+                                            'fields' => $fields));
+            if (!isset($result[$uid])) {
+                return PEAR::raiseError('No such contact!');
+            }
+            $list = Kolab_List::singleton();
+            $share = &$list->getByShare($result[$uid][0]['source'], 'contact');
+            if (is_a($share, 'PEAR_Error')) {
+                return $share;
+            }
+            return $share->getData();
+        }
+    }
+
+    function _getAppUids($app)
+    {
+        global $registry;
+
+        switch ($app) {
+        case 'turba':
+            $sources = $registry->call('contacts/sources',
+                                       array('writeable' => true));
+            $result = $registry->call('contacts/search',
+                                      array('names' => '',
+                                            'sources' => array_keys($sources),
+                                            'fields' => array()));
+            $uids = array();
+            foreach ($result[''] as $contact) {
+                if (isset($contact['__uid'])) {
+                    $uids[] = $contact['__uid'];
+                }
+            }
+            return $uids;
+        }
+    }
+
+    /**
+     * Connecting is not required for this driver.
+     *
+     * @access private
+     *
+     * @return NULL
+     */
+    function _connect()
+    {
+    }
+}
diff --git a/framework/VFS/lib/VFS/musql.php b/framework/VFS/lib/VFS/musql.php
new file mode 100644 (file)
index 0000000..4465d47
--- /dev/null
@@ -0,0 +1,554 @@
+<?php
+
+require_once dirname(__FILE__) . '/sql.php';
+
+/**
+ * Permission for read access.
+ */
+define('VFS_FLAG_READ', 1);
+
+/**
+ * Permission for read access.
+ */
+define('VFS_FLAG_WRITE', 2);
+
+/**
+ * Multi User VFS implementation for PHP's PEAR database
+ * abstraction layer.
+ *
+ * Required values for $params:<pre>
+ *      'phptype'       The database type (ie. 'pgsql', 'mysql', etc.).</pre>
+ *
+ * Optional values:<pre>
+ *      'table'         The name of the vfs table in 'database'. Defaults to
+ *                      'horde_muvfs'.</pre>
+ *
+ * Required by some database implementations:<pre>
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'database'      The name of the database.
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.</pre>
+ *
+ * Known Issues:
+ * Delete is not recusive, so files and folders that used to be in a folder
+ * that gets deleted live forever in the database, or re-appear when the folder
+ * is recreated.
+ * Rename has the same issue, so files are lost if a folder is renamed.
+ *
+ * The table structure for the VFS can be found in
+ * data/muvfs.sql.
+ *
+ * Database specific notes:
+ *
+ * MSSQL:
+ * - The vfs_data field must be of type IMAGE.
+ * - You need the following php.ini settings:
+ * <code>
+ *    ; Valid range 0 - 2147483647. Default = 4096.
+ *    mssql.textlimit = 0 ; zero to pass through
+ *
+ *    ; Valid range 0 - 2147483647. Default = 4096.
+ *    mssql.textsize = 0 ; zero to pass through
+ * </code>
+ *
+ * $Horde: framework/VFS/lib/VFS/musql.php,v 1.4 2009/01/06 17:49:58 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Mike Cochrane <mike@graftonhall.co.nz>
+ * @package VFS
+ */
+class VFS_musql extends VFS_sql {
+
+    /**
+     * List of permissions and if they can be changed in this VFS
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => false, 'write' => false, 'execute' => false),
+        'group' => array('read' => false, 'write' => false, 'execute' => false),
+        'all'   => array('read' => true,  'write' => true,  'execute' => false)
+    );
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        /* Make sure we have write access to this and all parent paths. */
+        if ($path != '') {
+            $paths = explode('/', $path);
+            $path_name = array_pop($paths);
+            if (!$this->isFolder(implode('/', $paths), $path_name)) {
+                if (!$autocreate) {
+                    return PEAR::raiseError(
+                        sprintf(_("Folder \"%s\" does not exist"), $path),
+                        'horde.error');
+                } else {
+                    $result = $this->autocreatePath($path);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                }
+            }
+            $paths[] = $path_name;
+            $previous = '';
+
+            foreach ($paths as $thispath) {
+                $sql = sprintf('SELECT vfs_owner, vfs_perms FROM %s
+                                WHERE vfs_path = ? AND vfs_name= ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $results = $this->_db->getAll($sql, array($previous, $thispath));
+                if (is_a($results, 'PEAR_Error')) {
+                    $this->log($results, PEAR_LOG_ERR);
+                    return $results;
+                }
+                if (!is_array($results) || count($results) < 1) {
+                    return PEAR::raiseError(_("Unable to create VFS file."));
+                }
+
+                $allowed = false;
+                foreach ($results as $result) {
+                    if ($result[0] == $this->_params['user'] ||
+                        $result[1] & VFS_FLAG_WRITE) {
+                        $allowed = true;
+                        break;
+                    }
+                }
+
+                if (!$allowed) {
+                    return PEAR::raiseError(_("Access denied creating VFS file."));
+                }
+
+                $previous = $thispath;
+            }
+        }
+
+        return parent::writeData($path, $name, $data, $autocreate);
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @param string $path  The path to store the file in.
+     * @param string $name  The filename to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        $sql = sprintf('SELECT vfs_id, vfs_owner, vfs_perms FROM %s
+                        WHERE vfs_path = ? AND vfs_name= ? AND vfs_type = ?',
+                       $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll($sql, array($path, $name, VFS_FILE));
+
+        if (is_a($fileList, 'PEAR_Error')) {
+            $this->log($fileList, PEAR_LOG_ERR);
+            return $fileList;
+        }
+        if (!is_array($fileList) || count($fileList) < 1) {
+            return PEAR::raiseError(_("Unable to delete VFS file."));
+        }
+
+        /* There may be one or more files with the same name but the user may
+         * not have read access to them, so doesn't see them. So we have to
+         * delete the one they have access to. */
+        foreach ($fileList as $file) {
+            if ($file[1] == $this->_params['user'] ||
+                $file[2] & VFS_FLAG_WRITE) {
+                $sql = sprintf('DELETE FROM %s WHERE vfs_id = ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $result = $this->_db->query($sql, array($file[0]));
+
+                if (is_a($result, 'PEAR_Error')) {
+                    $this->log($result, PEAR_LOG_ERR);
+                    return $result;
+                }
+                if ($this->_db->affectedRows() == 0) {
+                    return PEAR::raiseError(_("Unable to delete VFS file."));
+                }
+                return $result;
+            }
+        }
+
+        // FIXME: 'Access Denied deleting file %s/%s'
+        return PEAR::raiseError(_("Unable to delete VFS file."));
+    }
+
+    /**
+     * Renames a file or folder in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        $sql = sprintf('SELECT vfs_id, vfs_owner, vfs_perms FROM %s
+                        WHERE vfs_path = ? AND vfs_name= ?',
+                       $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll($sql, array($oldpath, $oldname));
+
+        if (is_a($fileList, 'PEAR_Error')) {
+            $this->log($fileList, PEAR_LOG_ERR);
+            return $fileList;
+        }
+        if (!is_array($fileList) || count($fileList) < 1) {
+            return PEAR::raiseError(_("Unable to rename VFS file."));
+        }
+
+        if (strpos($newpath, '/') === false) {
+            $parent = '';
+            $path = $newpath;
+        } else {
+            list($parent, $path) = explode('/', $newpath, 2);
+        }
+        if (!$this->isFolder($parent, $path)) {
+            if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        /* There may be one or more files with the same name but the user may
+         * not have read access to them, so doesn't see them. So we have to
+         * rename the one they have access to. */
+        foreach ($fileList as $file) {
+            if ($file[1] == $this->_params['user'] ||
+                $file[2] & VFS_FLAG_WRITE) {
+                $sql = sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?, vfs_modified = ?
+                                WHERE vfs_id = ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $result = $this->_db->query(
+                    $sql,
+                    array($newpath, $newname, time(), $file[0]));
+                if (is_a($result, 'PEAR_Error')) {
+                    $this->log($result, PEAR_LOG_ERR);
+                    return $result;
+                }
+                return $result;
+            }
+        }
+
+        return PEAR::raiseError(sprintf(_("Unable to rename VFS file %s/%s."),
+                                        $oldpath, $oldname));
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  Holds the path of directory to create folder.
+     * @param string $name  Holds the name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        /* Make sure we have write access to this and all parent paths. */
+        if (strlen($path)) {
+            $paths = explode('/', $path);
+            $previous = '';
+
+            foreach ($paths as $thispath) {
+                $sql = sprintf('SELECT vfs_owner, vfs_perms FROM %s
+                                WHERE vfs_path = ? AND vfs_name= ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $results = $this->_db->getAll($sql, array($previous, $thispath));
+                if (is_a($results, 'PEAR_Error')) {
+                    $this->log($results, PEAR_LOG_ERR);
+                    return $results;
+                }
+                if (!is_array($results) || count($results) < 1) {
+                    return PEAR::raiseError(_("Unable to create VFS directory."));
+                }
+
+                $allowed = false;
+                foreach ($results as $result) {
+                    if ($result[0] == $this->_params['user'] ||
+                        $result[1] & VFS_FLAG_WRITE) {
+                        $allowed = true;
+                        break;
+                    }
+                }
+
+                if (!$allowed) {
+                    return PEAR::raiseError(_("Access denied creating VFS directory."));
+                }
+
+                $previous = $thispath;
+            }
+        }
+
+        $id = $this->_db->nextId($this->_params['table']);
+        $sql = sprintf('INSERT INTO %s
+                        (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner, vfs_perms)
+                        VALUES (?, ?, ?, ?, ?, ?, ?)',
+                       $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $result = $this->_db->query(
+            $sql,
+            array($id, VFS_FOLDER, $path, $name, time(), $this->_params['user'], 0));
+        var_dump($this->_db->last_query);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->log($result, PEAR_LOG_ERR);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path        The path to delete the folder from.
+     * @param string $name        The foldername to use.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        if ($recursive) {
+            $result = $this->emptyFolder($path . '/' . $name);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $list = $this->listFolder($path . '/' . $name);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+            if (count($list)) {
+                return PEAR::raiseError(
+                    sprintf(_("Unable to delete %s, the directory is not empty"),
+                            $path . '/' . $name));
+            }
+        }
+
+        $sql = sprintf('SELECT vfs_id, vfs_owner, vfs_perms FROM %s
+                        WHERE vfs_path = ? AND vfs_name= ? AND vfs_type = ?',
+                       $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll($sql, array($path, $name, VFS_FOLDER));
+
+        if (is_a($fileList, 'PEAR_Error')) {
+            $this->log($fileList, PEAR_LOG_ERR);
+            return $fileList;
+        }
+        if (!is_array($fileList) || count($fileList) < 1) {
+            return PEAR::raiseError(_("Unable to delete VFS directory."));
+        }
+
+        /* There may be one or more folders with the same name but as the user
+         * may not have read access to them, they don't see them. So we have
+         * to delete the one they have access to */
+        foreach ($fileList as $file) {
+            if ($file[1] == $this->_params['user'] ||
+                $file[2] & VFS_FLAG_WRITE) {
+                $sql = sprintf('DELETE FROM %s WHERE vfs_id = ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $result = $this->_db->query($sql, array($file[0]));
+
+                if (is_a($result, 'PEAR_Error')) {
+                    $this->log($result, PEAR_LOG_ERR);
+                    return $result;
+                }
+                if ($this->_db->affectedRows() == 0) {
+                    return PEAR::raiseError(_("Unable to delete VFS directory."));
+                }
+
+                return $result;
+            }
+        }
+
+        // FIXME: 'Access Denied deleting folder %s/%s'
+        return PEAR::raiseError(_("Unable to delete VFS directory."));
+    }
+
+    /**
+     * Returns a list of the contents of a folder.
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return mixed  File list on success or false on failure.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                        $dironly = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $length_op = $this->_getFileSizeOp();
+        $sql = sprintf('SELECT vfs_name, vfs_type, vfs_modified, vfs_owner, vfs_perms, %s(vfs_data) FROM %s
+                        WHERE vfs_path = ? AND (vfs_owner = ? OR vfs_perms \&\& ?)',
+                       $length_op, $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll(
+            $sql,
+            array($path, $this->_params['user'], VFS_FLAG_READ));
+        if (is_a($fileList, 'PEAR_Error')) {
+            $this->log($fileList, PEAR_LOG_ERR);
+            return $fileList;
+        }
+
+        $files = array();
+        foreach ($fileList as $line) {
+            // Filter out dotfiles if they aren't wanted.
+            if (!$dotfiles && substr($line[0], 0, 1) == '.') {
+                continue;
+            }
+
+            $file['name'] = stripslashes($line[0]);
+
+            if ($line[1] == VFS_FILE) {
+                $name = explode('.', $line[0]);
+
+                if (count($name) == 1) {
+                    $file['type'] = '**none';
+                } else {
+                    $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                }
+
+                $file['size'] = $line[5];
+            } elseif ($line[1] == VFS_FOLDER) {
+                $file['type'] = '**dir';
+                $file['size'] = -1;
+            }
+
+            $file['date'] = $line[2];
+            $file['owner'] = $line[3];
+
+            $line[4] = intval($line[4]);
+            $file['perms']  = ($line[1] == VFS_FOLDER) ? 'd' : '-';
+            $file['perms'] .= 'rw-';
+            $file['perms'] .= ($line[4] & VFS_FLAG_READ) ? 'r' : '-';
+            $file['perms'] .= ($line[4] & VFS_FLAG_WRITE) ? 'w' : '-';
+            $file['perms'] .= '-';
+            $file['group'] = '';
+
+            // Filtering.
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+        }
+
+        return $files;
+    }
+
+    /**
+     * Changes permissions for an Item on the VFS.
+     *
+     * @param string $path  Holds the path of directory of the Item.
+     * @param string $name  Holds the name of the Item.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $name, $permission)
+    {
+        $conn = $this->_connect();
+        if (PEAR::isError($conn)) {
+            return $conn;
+        }
+
+        $val = intval(substr($permission, -1));
+        $perm = 0;
+        $perm |= ($val & 4) ? VFS_FLAG_READ : 0;
+        $perm |= ($val & 2) ? VFS_FLAG_WRITE : 0;
+
+        $sql = sprintf('SELECT vfs_id, vfs_owner, vfs_perms FROM %s
+                        WHERE vfs_path = ? AND vfs_name= ?',
+                       $this->_params['table']);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll($sql, array($path, $name));
+
+        if (is_a($fileList, 'PEAR_Error')) {
+            $this->log($fileList, PEAR_LOG_ERR);
+            return $fileList;
+        }
+        if (!is_array($fileList) || count($fileList) < 1) {
+            return PEAR::raiseError(_("Unable to rename VFS file."));
+        }
+
+        /* There may be one or more files with the same name but the user may
+         * not have read access to them, so doesn't see them. So we have to
+         * chmod the one they have access to. */
+        foreach ($fileList as $file) {
+            if ($file[1] == $this->_params['user'] ||
+                $file[2] & VFS_FLAG_WRITE) {
+                $sql = sprintf('UPDATE %s SET vfs_perms = ?
+                                WHERE vfs_id = ?',
+                               $this->_params['table']);
+                $this->log($sql, PEAR_LOG_DEBUG);
+                $result = $this->_db->query($sql, array($perm, $file[0]));
+                return $result;
+            }
+        }
+
+        return PEAR::raiseError(
+            sprintf(_("Unable to change permission for VFS file %s/%s."),
+                    $path, $name));
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/smb.php b/framework/VFS/lib/VFS/smb.php
new file mode 100644 (file)
index 0000000..23496de
--- /dev/null
@@ -0,0 +1,713 @@
+<?php
+/**
+ * Stateless VFS implementation for a SMB server, based on smbclient.
+ *
+ * Required values for $params:
+ * <pre>
+ *   'username'  - The username with which to connect to the SMB server.
+ *   'password'  - The password with which to connect to the SMB server.
+ *   'hostspec'  - The SMB server to connect to.
+ *   'port'      - The SMB port number to connect to.
+ *   'share'     - The share to access on the SMB server.
+ *   'smbclient' - The path to the 'smbclient' executable.
+ * </pre>
+ *
+ * Optional values for $params:
+ * <pre>
+ *   'ipaddress' - The address of the server to connect to.
+ * </pre>
+ *
+ * Functions not implemented:
+ *   - changePermissions(): The SMB permission style does not fit with the
+ *                          module.
+ *
+ * $Horde: framework/VFS/lib/VFS/smb.php,v 1.6 2009/05/31 17:33:52 jan Exp $
+ *
+ * Codebase copyright 2002 Paul Gareau <paul@xhawk.net>.  Adapted with
+ * permission by Patrice Levesque <wayne@ptaff.ca> from phpsmb-0.8 code, and
+ * converted to the LGPL.  Please do not taunt original author, contact
+ * Patrice Levesque or dev@lists.horde.org.
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Paul Gareau <paul@xhawk.net>
+ * @author  Patrice Levesque <wayne@ptaff.ca>
+ * @since   Horde 3.1
+ * @package VFS
+ */
+class VFS_smb extends VFS {
+
+    /**
+     * List of additional credentials required for this VFS backend.
+     *
+     * @var array
+     */
+    var $_credentials = array('username', 'password');
+
+    /**
+     * List of permissions and if they can be changed in this VFS backend.
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => false, 'write' => false, 'execute' => false),
+        'group' => array('read' => false, 'write' => false, 'execute' => false),
+        'all'   => array('read' => false, 'write' => false, 'execute' => false));
+
+    /**
+     * Authenticates a user on the SMB server and share.
+     *
+     * @access private
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function _connect()
+    {
+        $cmd = array('quit');
+        $err = $this->_command('', $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return PEAR::raiseError(_("Authentication to the SMB server failed."));
+        }
+        return true;
+    }
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        $size = filesize($file);
+        if ($size === 0) {
+            return '';
+        }
+
+        return file_get_contents($file);
+    }
+
+    /**
+     * Retrieves a file from the VFS as an on-disk local file.
+     *
+     * This function provides a file on local disk with the data of a VFS file
+     * in it. This file <em>cannot</em> be modified! The behavior if you do
+     * modify it is undefined. It will be removed at the end of the request.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string A local filename.
+     */
+    function readFile($path, $name)
+    {
+        // Create a temporary file and register it for deletion at the
+        // end of this request.
+        $localFile = $this->_getTempFile();
+        if (!$localFile) {
+            return PEAR::raiseError(_("Unable to create temporary file."));
+        }
+        register_shutdown_function(create_function('', 'unlink(\'' . addslashes($localFile) . '\');'));
+
+        list($path, $name) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('get \"' . $name . '\" ' . $localFile);
+        $result = $this->_command($path, $cmd);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        if (!file_exists($localFile)) {
+            return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return $localFile;
+    }
+
+    /**
+     * Open a stream to a file in the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return resource  The stream.
+     */
+    function readStream($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        $mode = OS_WINDOWS ? 'rb' : 'r';
+        return fopen($file, $mode);
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to be
+     *                             stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        // Double quotes not allowed in SMB filename.
+        $name = str_replace('"', "'", $name);
+
+        list($path, $name) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('put \"' . $tmpFile . '\" \"' . $name . '\"');
+        // do we need to first autocreate the directory?
+        if ($autocreate) {
+            $result = $this->autocreatePath($path);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        $err = $this->_command($path, $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return $err;
+        }
+        return true;
+    }
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $tmpFile = $this->_getTempFile();
+        $fp = fopen($tmpFile, 'wb');
+        fwrite($fp, $data);
+        fclose($fp);
+        $result = $this->write($path, $name, $tmpFile, $autocreate);
+        unlink($tmpFile);
+        return $result;
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to use.
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        // In some samba versions after samba-3.0.25-pre2, $path must
+        // end in a trailing slash.
+        if (substr($path, -1) != '/') {
+            $path .= '/';
+        }
+
+        list($path, $name) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('del \"' . $name . '\"');
+        $err = $this->_command($path, $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return $err;
+        }
+        return true;
+    }
+
+    /**
+     * Checks if a given pathname is a folder.
+     *
+     * @param string $path  The path to the folder.
+     * @param string $name  The file or folder name.
+     *
+     * @return boolean  True if it is a folder, false otherwise.
+     */
+    function isFolder($path, $name)
+    {
+        list($path, $name) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('quit');
+        $err = $this->_command($this->_getPath($path, $name), $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path        The path to delete the folder from.
+     * @param string $name        The name of the folder to delete.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        // In some samba versions after samba-3.0.25-pre2, $path must
+        // end in a trailing slash.
+        if (substr($path, -1) != '/') {
+            $path .= '/';
+        }
+
+        if (!$this->isFolder($path, $name)) {
+            return PEAR::raiseError(sprintf(_("\"%s\" is not a directory."), $path . '/' . $name));
+        }
+
+        $file_list = $this->listFolder($this->_getPath($path, $name));
+        if (is_a($file_list, 'PEAR_Error')) {
+            return $file_list;
+        }
+
+        if ($file_list && !$recursive) {
+            return PEAR::raiseError(sprintf(_("Unable to delete \"%s\", the directory is not empty."),
+                                            $this->_getPath($path, $name)));
+        }
+
+        foreach ($file_list as $file) {
+            if ($file['type'] == '**dir') {
+                $result = $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
+            } else {
+                $result = $this->deleteFile($this->_getPath($path, $name), $file['name']);
+            }
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        // Really delete the folder.
+        list($path, $name) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('rmdir \"' . $name . '\"');
+        $err = $this->_command($path, $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS folder \"%s\"."), $this->_getPath($path, $name)));
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Renames a file in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Double quotes not allowed in SMB filename. The '/' character should
+        // also be removed from the beginning/end of the names.
+        $oldname = str_replace('"', "'", trim($oldname, '/'));
+        $newname = str_replace('"', "'", trim($newname, '/'));
+
+        if (empty($oldname)) {
+            return PEAR::raiseError(_("Unable to rename VFS file to same name."));
+        }
+
+        /* If the path was not empty (i.e. the path is not the root path),
+         * then add the trailing '/' character to path. */
+        if (!empty($oldpath)) {
+            $oldpath .= '/';
+        }
+        if (!empty($newpath)) {
+            $newpath .= '/';
+        }
+
+        list($file, $name) = $this->_escapeShellCommand($oldname, $newname);
+        $cmd = array('rename \"' .  str_replace('/', '\\\\', $oldpath) . $file . '\" \"' .
+                                    str_replace('/', '\\\\', $newpath) . $name . '\"');
+        if (is_a($err = $this->_command('', $cmd), 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Unable to rename VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  The path of directory to create folder.
+     * @param string $name  The name of the new folder.
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function createFolder($path, $name)
+    {
+        // In some samba versions after samba-3.0.25-pre2, $path must
+        // end in a trailing slash.
+        if (substr($path, -1) != '/') {
+            $path .= '/';
+        }
+
+        // Double quotes not allowed in SMB filename.
+        $name = str_replace('"', "'", $name);
+
+        list($dir, $mkdir) = $this->_escapeShellCommand($path, $name);
+        $cmd = array('mkdir \"' . $mkdir . '\"');
+        $err = $this->_command($dir, $cmd);
+        if (is_a($err, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Unable to create VFS folder \"%s\"."), $this->_getPath($path, $name)));
+        }
+        return true;
+    }
+
+    /**
+     * Returns an unsorted file list.
+     *
+     * @param string $path       The path of the directory to get the file list
+     *                           for.
+     * @param mixed $filter      Hash of items to filter based on filename.
+     * @param boolean $dotfiles  Show dotfiles? This is irrelevant with
+     *                           smbclient.
+     * @param boolean $dironly   Show directories only?
+     *
+     * @return boolean|PEAR_Error  File list on success or a PEAR_Error on
+     *                             failure.
+     */
+    function listFolder($path = '', $filter = null, $dotfiles = true, $dironly = false)
+    {
+        list($path) = $this->_escapeShellCommand($path);
+        $cmd = array('ls');
+        $res = $this->_command($path, $cmd);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+        return $this->parseListing($res, $filter, $dotfiles, $dironly);
+    }
+
+    function parseListing($res, $filter, $dotfiles, $dironly)
+    {
+        $num_lines = count($res);
+        $files = array();
+        for ($r = 0; $r < $num_lines; $r++) {
+            // Match file listing.
+            if (!preg_match('/^(\s\s.+\s{6,})/', $res[$r])) {
+                continue;
+            }
+
+            // Split into columns at every six spaces
+            $split1 = preg_split('/\s{6,}/', trim($res[$r]));
+            // If the file name isn't . or ..
+            if ($split1[0] == '.' || $split1[0] == '..') {
+                continue;
+            }
+
+            if (isset($split1[2])) {
+                // If there is a small file size, inf could be split
+                // into 3 cols.
+                $split1[1] .= ' ' . $split1[2];
+            }
+            // Split file inf at every one or more spaces.
+            $split2 = preg_split('/\s+/', $split1[1]);
+            if (is_numeric($split2[0])) {
+                // If there is no file attr, shift cols over.
+                array_unshift($split2, '');
+            }
+            $my_name = $split1[0];
+
+            // Filter out dotfiles if they aren't wanted.
+            if (!$dotfiles && substr($my_name, 0, 1) == '.') {
+                continue;
+            }
+
+            $my_size = $split2[1];
+            $ext_name = explode('.', $my_name);
+
+            if ((strpos($split2[0], 'D') !== false)) {
+                $my_type = '**dir';
+                $my_size = -1;
+            } else {
+                $my_type = VFS::strtolower($ext_name[count($ext_name) - 1]);
+            }
+            $my_date = strtotime($split2[4] . ' ' . $split2[3] . ' ' .
+                                 $split2[6] . ' ' . $split2[5]);
+            $filedata = array('owner' => '',
+                              'group' => '',
+                              'perms' => '',
+                              'name' => $my_name,
+                              'type' => $my_type,
+                              'date' => $my_date,
+                              'size' => $my_size);
+            // watch for filters and dironly
+            if ($this->_filterMatch($filter, $my_name)) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $my_type !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$filedata['name']] = $filedata;
+        }
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders? Irrelevant for SMB.
+     *
+     * @return boolean|PEAR_Error  Folder list on success or a PEAR_Error on
+     *                             failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        $folders = array();
+        $folder = array();
+
+        $folderList = $this->listFolder($path, null, $dotfolders, true);
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        // dirname will strip last component from path, even on a directory
+        $folder['val'] = dirname($path);
+        $folder['abbrev'] = '..';
+        $folder['label'] = '..';
+
+        $folders[$folder['val']] = $folder;
+
+        foreach ($folderList as $files) {
+            $folder['val'] = $this->_getPath($path, $files['name']);
+            $folder['abbrev'] = $files['name'];
+            $folder['label'] = $folder['val'];
+
+            $folders[$folder['val']] = $folder;
+        }
+
+        ksort($folders);
+        return $folders;
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $dest         The destination of the file.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."),
+                                                $this->_getPath($dest, $name)));
+            }
+        }
+
+        if ($this->isFolder($path, $name)) {
+            if (is_a($result = $this->_copyRecursive($path, $name, $dest), 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $tmpFile = $this->readFile($path, $name);
+            if (is_a($tmpFile, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Failed to retrieve: %s"), $orig));
+            }
+
+            $result = $this->write($dest, $name, $tmpFile);
+            if (is_a($result, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Copy failed: %s"),
+                                                $this->_getPath($dest, $name)));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Moves a file through the backend.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $dest         The destination of the file.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."),
+                                                $this->_getPath($dest, $name)));
+            }
+        }
+
+        $err = $this->rename($path, $name, $dest, $name);
+        if (is_a($err, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Failed to move to \"%s\"."),
+                                            $this->_getPath($dest, $name)));
+        }
+        return true;
+    }
+
+    /**
+     * Replacement for escapeshellcmd(), variable length args, as we only want
+     * certain characters escaped.
+     *
+     * @access private
+     *
+     * @param array $array  Strings to escape.
+     *
+     * @return array
+     */
+    function _escapeShellCommand()
+    {
+        $ret = array();
+        $args = func_get_args();
+        foreach ($args as $arg) {
+            $ret[] = str_replace(array(';', '\\'), array('\;', '\\\\'), $arg);
+        }
+        return $ret;
+    }
+
+    /**
+     * Executes a command and returns output lines in array.
+     *
+     * @access private
+     *
+     * @param string $cmd  Command to be executed
+     *
+     * @return mixed  Array on success, false on failure.
+     */
+    function _execute($cmd)
+    {
+        $cmd = str_replace('"-U%"', '-N', $cmd);
+        exec($cmd, $out, $ret);
+
+        // In some cases, (like trying to delete a nonexistant file),
+        // smbclient will return success (at least on 2.2.7 version I'm
+        // testing on). So try to match error strings, even after success.
+        if ($ret != 0) {
+            $err = '';
+            foreach ($out as $line) {
+                if (strpos($line, 'Usage:') === 0) {
+                    $err = 'Command syntax incorrect';
+                    break;
+                }
+                if (strpos($line, 'ERRSRV') !== false ||
+                    strpos($line, 'ERRDOS') !== false) {
+                    $err = preg_replace('/.*\((.+)\).*/', '\\1', $line);
+                    if (!$err) {
+                        $err = $line;
+                    }
+                    break;
+                }
+            }
+            if (!$err) {
+                $err = $out ? $out[count($out) - 1] : $ret;
+            }
+            return PEAR::raiseError($err);
+        }
+
+        // Check for errors even on success.
+        $err = '';
+        foreach ($out as $line) {
+            if (strpos($line, 'NT_STATUS_NO_SUCH_FILE') !== false ||
+                strpos($line, 'NT_STATUS_OBJECT_NAME_NOT_FOUND') !== false) {
+                $err = _("No such file");
+                break;
+            } elseif (strpos($line, 'NT_STATUS_ACCESS_DENIED') !== false) {
+                $err = _("Permission Denied");
+                break;
+            }
+        }
+
+        if ($err) {
+            return PEAR::raiseError($err);
+        }
+
+        return $out;
+    }
+
+    /**
+     * Executes SMB commands - without authentication - and returns output
+     * lines in array.
+     *
+     * @access private
+     *
+     * @param array $path  Base path for command.
+     * @param array $cmd   Commands to be executed.
+     *
+     * @return mixed  Array on success, false on failure.
+     */
+    function _command($path, $cmd)
+    {
+        list($share) = $this->_escapeShellCommand($this->_params['share']);
+        putenv('PASSWD=' . $this->_params['password']);
+        $ipoption = (isset($this->_params['ipaddress'])) ? (' -I ' . $this->_params['ipaddress']) : null;
+        $fullcmd = $this->_params['smbclient'] .
+            ' "//' . $this->_params['hostspec'] . '/' . $share . '"' .
+            ' "-p' . $this->_params['port'] . '"' .
+            ' "-U' . $this->_params['username'] . '"' .
+            ' -D "' . $path . '" ' .
+            $ipoption .
+            ' -c "';
+        foreach ($cmd as $c) {
+            $fullcmd .= $c . ";";
+        }
+        $fullcmd .= '"';
+        return $this->_execute($fullcmd);
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/sql.php b/framework/VFS/lib/VFS/sql.php
new file mode 100644 (file)
index 0000000..20b411f
--- /dev/null
@@ -0,0 +1,1118 @@
+<?php
+
+/**
+ * File value for vfs_type column.
+ */
+define('VFS_FILE', 1);
+
+/**
+ * Folder value for vfs_type column.
+ */
+define('VFS_FOLDER', 2);
+
+/**
+ * VFS implementation for PHP's PEAR database abstraction layer.
+ *
+ * Required values for $params:<pre>
+ *   'phptype'      The database type (ie. 'pgsql', 'mysql', etc.).</pre>
+ *
+ * Optional values:<pre>
+ *   'table'          The name of the vfs table in 'database'. Defaults to
+ *                    'horde_vfs'.</pre>
+ *
+ * Required by some database implementations:<pre>
+ *   'hostspec'     The hostname of the database server.
+ *   'protocol'     The communication protocol ('tcp', 'unix', etc.).
+ *   'database'     The name of the database.
+ *   'username'     The username with which to connect to the database.
+ *   'password'     The password associated with 'username'.
+ *   'options'      Additional options to pass to the database.
+ *   'tty'          The TTY on which to connect to the database.
+ *   'port'         The port on which to connect to the database.</pre>
+ *
+ * Optional values when using separate reading and writing servers, for example
+ * in replication settings:<pre>
+ *   'splitread'   Boolean, whether to implement the separation or not.
+ *   'read'        Array containing the parameters which are different for
+ *                 the read database connection, currently supported
+ *                 only 'hostspec' and 'port' parameters.</pre>
+ *
+ * The table structure for the VFS can be found in data/vfs.sql.
+ *
+ * Database specific notes:
+ *
+ * MSSQL:
+ * <pre>
+ * - The vfs_data field must be of type IMAGE.
+ * - You need the following php.ini settings:
+ *    ; Valid range 0 - 2147483647. Default = 4096.
+ *    mssql.textlimit = 0 ; zero to pass through
+ *
+ *    ; Valid range 0 - 2147483647. Default = 4096.
+ *    mssql.textsize = 0 ; zero to pass through
+ * </pre>
+ *
+ * $Horde: framework/VFS/lib/VFS/sql.php,v 1.6 2009/02/13 05:35:08 chuck Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package VFS
+ */
+class VFS_sql extends VFS {
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db = false;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $_db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * Boolean indicating whether or not we're connected to the SQL
+     * server.
+     *
+     * @var boolean
+     */
+    var $_connected = false;
+
+    /**
+     * Retrieves the filesize from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return int The file size.
+     */
+    function size($path, $name)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $length_op = $this->_getFileSizeOp();
+        $sql = sprintf(
+            'SELECT %s(vfs_data) FROM %s WHERE vfs_path = ? AND vfs_name = ?',
+            $length_op,
+            $this->_params['table']
+        );
+        $values = array($this->_convertPath($path), $name);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $size = $this->_db->getOne($sql, $values);
+
+        if (is_null($size)) {
+            return PEAR::raiseError(sprintf(_("Unable to check file size of \"%s/%s\"."), $path, $name));
+        }
+
+        return $size;
+    }
+
+    /**
+     * Returns the size of a file.
+     *
+     * @access public
+     *
+     * @param string $path  The path of the file.
+     * @param string $name  The filename.
+     *
+     * @return integer  The size of the folder in bytes or PEAR_Error on
+     *                  failure.
+     */
+    function getFolderSize($path = null, $name = null)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $where = (is_null($path)) ? null : sprintf('WHERE vfs_path LIKE %s', ((!strlen($path)) ? '""' : $this->_db->quote($this->_convertPath($path) . '%')));
+        $length_op = $this->_getFileSizeOp();
+        $sql = sprintf(
+            'SELECT SUM(%s(vfs_data)) FROM %s %s',
+            $length_op,
+            $this->_params['table'],
+            $where
+        );
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $size = $this->_db->getOne($sql);
+
+        return $size !== null ? $size : 0;
+    }
+
+    /**
+     * Retrieve a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $this->_readBlob($this->_params['table'], 'vfs_data',
+                                array('vfs_path' => $this->_convertPath($path),
+                                      'vfs_name' => $name));
+    }
+
+    /**
+     * Retrieves a part of a file from the VFS. Particularly useful
+     * when reading large files which would exceed the PHP memory
+     * limits if they were stored in a string.
+     *
+     * @param string  $path       The pathname to the file.
+     * @param string  $name       The filename to retrieve.
+     * @param integer $offset     The offset of the part. (The new offset will be
+     *                            stored in here).
+     * @param integer $length     The length of the part. If the length = -1, the
+     *                            whole part after the offset is retrieved. If
+     *                            more bytes are given as exists after the given
+     *                            offset. Only the available bytes are read.
+     * @param integer $remaining  The bytes that are left, after the part that is
+     *                            retrieved.
+     *
+     * @return string The file data.
+     */
+    function readByteRange($path, $name, &$offset, $length = -1, &$remaining)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $data = $this->_readBlob($this->_params['table'], 'vfs_data',
+                                 array('vfs_path' => $this->_convertPath($path),
+                                       'vfs_name' => $name));
+
+        // Calculate how many bytes MUST be read, so the remainging
+        // bytes and the new offset can be calculated correctly.
+        $size = strlen ($data);
+        if ($length == -1 || (($length + $offset) > $size)) {
+            $length = $size - $offset;
+        }
+        if ($remaining < 0) {
+            $remaining = 0;
+        }
+
+        $data = substr($data, $offset, $length);
+        $offset = $offset + $length;
+        $remaining = $size - $offset;
+
+        return $data;
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        /* Don't need to check quota here since it will be checked when
+         * writeData() is called. */
+        return $this->writeData($path,
+                                $name,
+                                file_get_contents($tmpFile),
+                                $autocreate);
+    }
+
+    /**
+     * Store a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $result = $this->_checkQuotaWrite('string', $data);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $path = $this->_convertPath($path);
+
+        /* Check to see if the data already exists. */
+        $sql = sprintf('SELECT vfs_id FROM %s WHERE vfs_path %s AND vfs_name = ?',
+                       $this->_params['table'],
+                       (!strlen($path) && $this->_db->dbsyntax == 'oci8') ? ' IS NULL' : ' = ' . $this->_db->quote($path));
+        $values = array($name);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $id = $this->_db->getOne($sql, $values);
+
+        if (is_a($id, 'PEAR_Error')) {
+            $this->log($id, PEAR_LOG_ERR);
+            return $id;
+        }
+
+        if (!is_null($id)) {
+            return $this->_updateBlob($this->_params['table'], 'vfs_data',
+                                      $data, array('vfs_id' => $id),
+                                      array('vfs_modified' => time()));
+        } else {
+            /* Check to see if the folder already exists. */
+            $dirs = explode('/', $path);
+            $path_name = array_pop($dirs);
+            $parent = implode('/', $dirs);
+            if (!$this->isFolder($parent, $path_name)) {
+                if (!$autocreate) {
+                    return PEAR::raiseError(sprintf(_("Folder \"%s\" does not exist"), $path), 'horde.error');
+                } else {
+                    $result = $this->autocreatePath($path);
+                    if (is_a($result, 'PEAR_Error')) {
+                        return $result;
+                    }
+                }
+            }
+
+            $id = $this->_write_db->nextId($this->_params['table']);
+            if (is_a($id, 'PEAR_Error')) {
+                $this->log($id, PEAR_LOG_ERR);
+                return $id;
+            }
+
+            return $this->_insertBlob($this->_params['table'], 'vfs_data',
+                                      $data, array('vfs_id' => $id,
+                                                   'vfs_type' => VFS_FILE,
+                                                   'vfs_path' => $path,
+                                                   'vfs_name' => $name,
+                                                   'vfs_modified' => time(),
+                                                   'vfs_owner' => $this->_params['user']));
+        }
+    }
+
+    /**
+     * Delete a file from the VFS.
+     *
+     * @param string $path  The path to store the file in.
+     * @param string $name  The filename to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $result = $this->_checkQuotaDelete($path, $name);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $path = $this->_convertPath($path);
+
+        $sql = sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path %s AND vfs_name = ?',
+                       $this->_params['table'],
+                       (!strlen($path) && $this->_db->dbsyntax == 'oci8') ? ' IS NULL' : ' = ' . $this->_db->quote($path));
+        $values = array(VFS_FILE, $name);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $result = $this->_db->query($sql, $values);
+
+        if ($this->_db->affectedRows() == 0) {
+            return PEAR::raiseError(_("Unable to delete VFS file."));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Rename a file or folder in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (strpos($newpath, '/') === false) {
+            $parent = '';
+            $path = $newpath;
+        } else {
+            list($parent, $path) = explode('/', $newpath, 2);
+        }
+        if (!$this->isFolder($parent, $path)) {
+            if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $oldpath = $this->_convertPath($oldpath);
+        $newpath = $this->_convertPath($newpath);
+
+        $sql  = 'UPDATE ' . $this->_params['table'];
+        $sql .= ' SET vfs_path = ?, vfs_name = ?, vfs_modified = ? WHERE vfs_path = ? AND vfs_name = ?';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array($newpath, $newname, time(), $oldpath, $oldname);
+
+        $result = $this->_write_db->query($sql, $values);
+
+        if ($this->_write_db->affectedRows() == 0) {
+            return PEAR::raiseError(_("Unable to rename VFS file."));
+        }
+
+        $rename = $this->_recursiveRename($oldpath, $oldname, $newpath, $newname);
+        if (is_a($rename, 'PEAR_Error')) {
+            $this->log($rename, PEAR_LOG_ERR);
+            return PEAR::raiseError(sprintf(_("Unable to rename VFS directory: %s."), $rename->getMessage()));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  Holds the path of directory to create folder.
+     * @param string $name  Holds the name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $id = $this->_write_db->nextId($this->_params['table']);
+        if (is_a($id, 'PEAR_Error')) {
+            $this->log($id, PEAR_LOG_ERR);
+            return $id;
+        }
+
+        $sql  = 'INSERT INTO ' . $this->_params['table'];
+        $sql .= ' (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner) VALUES (?, ?, ?, ?, ?, ?)';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array($id, VFS_FOLDER, $this->_convertPath($path), $name, time(), $this->_params['user']);
+
+        return $this->_db->query($sql, $values);
+    }
+
+    /**
+     * Delete a folder from the VFS.
+     *
+     * @param string $path        The path of the folder.
+     * @param string $name        The folder name to use.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $path = $this->_convertPath($path);
+
+        $folderPath = $this->_getNativePath($path, $name);
+
+        /* Check if not recursive and fail if directory not empty */
+        if (!$recursive) {
+            $folderList = $this->listFolder($folderPath, null, true);
+            if (is_a($folderList, 'PEAR_Error')) {
+                $this->log($folderList, PEAR_LOG_ERR);
+                return $folderList;
+            } elseif (!empty($folderList)) {
+                return PEAR::raiseError(sprintf(_("Unable to delete %s, the directory is not empty"),
+                                                $path . '/' . $name));
+            }
+        }
+
+        /* First delete everything below the folder, so if error we
+         * get no orphans */
+        $sql = sprintf('DELETE FROM %s WHERE vfs_path %s',
+                       $this->_params['table'],
+                       (!strlen($folderPath) && $this->_write_db->dbsyntax == 'oci8') ? ' IS NULL' : ' LIKE ' . $this->_write_db->quote($this->_getNativePath($folderPath, '%')));
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $deleteContents = $this->_write_db->query($sql);
+        if (is_a($deleteContents, 'PEAR_Error')) {
+            $this->log($deleteContents, PEAR_LOG_ERR);
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS recursively: %s."), $deleteContents->getMessage()));
+        }
+
+        /* Now delete everything inside the folder. */
+        $sql = sprintf('DELETE FROM %s WHERE vfs_path %s',
+                       $this->_params['table'],
+                       (!strlen($path) && $this->_write_db->dbsyntax == 'oci8') ? ' IS NULL' : ' = ' . $this->_write_db->quote($folderPath));
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $delete = $this->_write_db->query($sql);
+        if (is_a($delete, 'PEAR_Error')) {
+            $this->log($delete, PEAR_LOG_ERR);
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS directory: %s."), $delete->getMessage()));
+        }
+
+        /* All ok now delete the actual folder */
+        $sql = sprintf('DELETE FROM %s WHERE vfs_path %s AND vfs_name = ?',
+                       $this->_params['table'],
+                       (!strlen($path) && $this->_write_db->dbsyntax == 'oci8') ? ' IS NULL' : ' = ' . $this->_write_db->quote($path));
+        $values = array($name);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $delete = $this->_write_db->query($sql, $values);
+        if (is_a($delete, 'PEAR_Error')) {
+            $this->log($delete, PEAR_LOG_ERR);
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS directory: %s."), $delete->getMessage()));
+        }
+
+        return $delete;
+    }
+
+    /**
+     * Return a list of the contents of a folder.
+     *
+     * @param string $path       The directory path.
+     * @param mixed $filter      String/hash of items to filter based on
+     *                           filename.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show directories only?
+     *
+     * @return mixed  File list on success or false on failure.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $path = $this->_convertPath($path);
+
+        // Fix for Oracle not differentiating between '' and NULL.
+        if (!strlen($path) && $this->_db->dbsyntax == 'oci8') {
+            $where = 'vfs_path IS NULL';
+        } else {
+            $where = 'vfs_path = ' . $this->_db->quote($path);
+        }
+
+        $length_op = $this->_getFileSizeOp();
+        $sql = sprintf('SELECT vfs_name, vfs_type, %s(vfs_data), vfs_modified, vfs_owner FROM %s WHERE %s',
+                       $length_op,
+                       $this->_params['table'],
+                       $where);
+        $this->log($sql, PEAR_LOG_DEBUG);
+        $fileList = $this->_db->getAll($sql);
+        if (is_a($fileList, 'PEAR_Error')) {
+            return $fileList;
+        }
+
+        $files = array();
+        foreach ($fileList as $line) {
+            // Filter out dotfiles if they aren't wanted.
+            if (!$dotfiles && substr($line[0], 0, 1) == '.') {
+                continue;
+            }
+
+            $file['name'] = $line[0];
+
+            if ($line[1] == VFS_FILE) {
+                $name = explode('.', $line[0]);
+
+                if (count($name) == 1) {
+                    $file['type'] = '**none';
+                } else {
+                    $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                }
+
+                $file['size'] = $line[2];
+            } elseif ($line[1] == VFS_FOLDER) {
+                $file['type'] = '**dir';
+                $file['size'] = -1;
+            }
+
+            $file['date'] = $line[3];
+            $file['owner'] = $line[4];
+            $file['perms'] = '';
+            $file['group'] = '';
+
+            // filtering
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+       }
+
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        String/hash of items to filter based on
+     *                             folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = array(), $dotfolders = true)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $path = $this->_convertPath($path);
+
+        $sql  = 'SELECT vfs_name, vfs_path FROM ' . $this->_params['table'];
+        $sql .= ' WHERE vfs_path = ? AND vfs_type = ?';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array($path, VFS_FOLDER);
+
+        $folderList = $this->_db->getAll($sql, $values);
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        $folders = array();
+        foreach ($folderList as $line) {
+            $folder['val'] = $this->_getNativePath($line[1], $line[0]);
+            $folder['abbrev'] = '';
+            $folder['label'] = '';
+
+            $count = substr_count($folder['val'], '/');
+
+            $x = 0;
+            while ($x < $count) {
+                $folder['abbrev'] .= '    ';
+                $folder['label'] .= '    ';
+                $x++;
+            }
+
+            $folder['abbrev'] .= $line[0];
+            $folder['label'] .= $line[0];
+
+            $strlen = VFS::strlen($folder['label']);
+            if ($strlen > 26) {
+                $folder['abbrev'] = substr($folder['label'], 0, ($count * 4));
+                $length = (29 - ($count * 4)) / 2;
+                $folder['abbrev'] .= substr($folder['label'], ($count * 4), $length);
+                $folder['abbrev'] .= '...';
+                $folder['abbrev'] .= substr($folder['label'], -1 * $length, $length);
+            }
+
+            $found = false;
+            foreach ($filter as $fltr) {
+                if ($folder['val'] == $fltr) {
+                    $found = true;
+                }
+            }
+
+            if (!$found) {
+                $folders[$folder['val']] = $folder;
+            }
+        }
+
+        ksort($folders);
+        return $folders;
+    }
+
+    /**
+     * Garbage collect files in the VFS storage system.
+     *
+     * @param string $path   The VFS path to clean.
+     * @param integer $secs  The minimum amount of time (in seconds) required
+     *                       before a file is removed.
+     */
+    function gc($path, $secs = 345600)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $sql = 'DELETE FROM ' . $this->_params['table']
+            . ' WHERE vfs_type = ? AND vfs_modified < ? AND (vfs_path = ? OR vfs_path LIKE ?)';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array(VFS_FILE,
+                        time() - $secs,
+                        $this->_convertPath($path),
+                        $this->_convertPath($path) . '/%');
+
+        return $this->_write_db->query($sql, $values);
+    }
+
+    /**
+     * Renames all child paths.
+     *
+     * @access private
+     *
+     * @param string $path  The path of the folder to rename.
+     * @param string $name  The foldername to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _recursiveRename($oldpath, $oldname, $newpath, $newname)
+    {
+        $oldpath = $this->_convertPath($oldpath);
+        $newpath = $this->_convertPath($newpath);
+
+        $sql  = 'SELECT vfs_name FROM ' . $this->_params['table'];
+        $sql .= ' WHERE vfs_type = ? AND vfs_path = ?';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array(VFS_FOLDER, $this->_getNativePath($oldpath, $oldname));
+
+        $folderList = $this->_db->getCol($sql, 0, $values);
+
+        foreach ($folderList as $folder) {
+            $this->_recursiveRename($this->_getNativePath($oldpath, $oldname), $folder, $this->_getNativePath($newpath, $newname), $folder);
+        }
+
+        $sql = 'UPDATE ' . $this->_params['table'] . ' SET vfs_path = ? WHERE vfs_path = ?';
+        $this->log($sql, PEAR_LOG_DEBUG);
+
+        $values = array($this->_getNativePath($newpath, $newname), $this->_getNativePath($oldpath, $oldname));
+
+        return $this->_write_db->query($sql, $values);
+    }
+
+    /**
+     * Return a full filename on the native filesystem, from a VFS
+     * path and name.
+     *
+     * @access private
+     *
+     * @param string $path  The VFS file path.
+     * @param string $name  The VFS filename.
+     *
+     * @return string  The full native filename.
+     */
+    function _getNativePath($path, $name)
+    {
+        if (!strlen($path)) {
+            return $name;
+        }
+
+        if (isset($this->_params['home']) &&
+            preg_match('|^~/?(.*)$|', $path, $matches)) {
+            $path = $this->_params['home'] . '/' . $matches[1];
+        }
+
+        return $path . '/' . $name;
+    }
+
+    /**
+     * Attempts to open a persistent connection to the SQL server.
+     *
+     * @access private
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _connect()
+    {
+        if ($this->_connected) {
+            return true;
+        }
+
+        if (!is_array($this->_params)) {
+            return PEAR::raiseError(_("No configuration information specified for SQL VFS."));
+        }
+
+        $required = array('phptype');
+        foreach ($required as $val) {
+            if (!isset($this->_params[$val])) {
+                return PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), $val));
+            }
+        }
+
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+        if (!isset($this->_params['table'])) {
+            $this->_params['table'] = 'horde_vfs';
+        }
+
+        /* Connect to the SQL server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_write_db = &DB::connect($this->_params,
+                                        array('persistent' => !empty($this->_params['persistent']),
+                                              'ssl' => !empty($this->_params['ssl'])));
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            $this->log($this->_write_db, PEAR_LOG_ERR);
+            $error = $this->_write_db;
+            $this->_write_db = false;
+            return $error;
+        }
+
+        // Set DB portability options.
+        switch ($this->_write_db->phptype) {
+        case 'mssql':
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+        default:
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+        }
+
+        /* Check if we need to set up the read DB connection
+         * seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = &DB::connect($params,
+                                      array('persistent' => !empty($params['persistent']),
+                                            'ssl' => !empty($params['ssl'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                return $this->_db;
+            }
+
+            // Set DB portability options.
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+
+        } else {
+            /* Default to the same DB handle for reads. */
+            $this->_db =& $this->_write_db;
+        }
+
+        $this->_connected = true;
+        return true;
+    }
+
+    /**
+     * Read file data from the SQL VFS backend.
+     *
+     * @access private
+     *
+     * @param string $table    The VFS table name.
+     * @param string $field    TODO
+     * @param array $criteria  TODO
+     *
+     * @return mixed  TODO
+     */
+    function _readBlob($table, $field, $criteria)
+    {
+        if (!count($criteria)) {
+            return PEAR::raiseError('You must specify the fetch criteria');
+        }
+
+        $where = '';
+
+        switch ($this->_db->dbsyntax) {
+        case 'oci8':
+            foreach ($criteria as $key => $value) {
+                if (!empty($where)) {
+                    $where .= ' AND ';
+                }
+                if (!strlen($value)) {
+                    $where .= $key . ' IS NULL';
+                } else {
+                    $where .= $key . ' = ' . $this->_db->quote($value);
+                }
+            }
+
+            $statement = OCIParse($this->_db->connection,
+                                  sprintf('SELECT %s FROM %s WHERE %s',
+                                          $field, $table, $where));
+            OCIExecute($statement);
+            if (OCIFetchInto($statement, $lob)) {
+                $result = $lob[0]->load();
+                if (is_null($result)) {
+                    $result = PEAR::raiseError('Unable to load SQL data.');
+                }
+            } else {
+                $result = PEAR::raiseError('Unable to load SQL data.');
+            }
+            OCIFreeStatement($statement);
+            break;
+
+        default:
+            foreach ($criteria as $key => $value) {
+                if (!empty($where)) {
+                    $where .= ' AND ';
+                }
+                $where .= $key . ' = ' . $this->_db->quote($value);
+            }
+
+            $sql = sprintf('SELECT %s FROM %s WHERE %s',
+                           $field, $table, $where);
+            $this->log($sql, PEAR_LOG_DEBUG);
+            $result = $this->_db->getOne($sql);
+
+            if (is_null($result)) {
+                $result = PEAR::raiseError('Unable to load SQL data.');
+            } else {
+                switch ($this->_db->dbsyntax) {
+                case 'pgsql':
+                    $result = pack('H' . strlen($result), $result);
+                    break;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * TODO
+     *
+     * @access private
+     *
+     * @param string $table       TODO
+     * @param string $field       TODO
+     * @param string $data        TODO
+     * @param string $attributes  TODO
+     *
+     * @return mixed  TODO
+     */
+    function _insertBlob($table, $field, $data, $attributes)
+    {
+        $fields = array();
+        $values = array();
+
+        switch ($this->_write_db->dbsyntax) {
+        case 'oci8':
+            foreach ($attributes as $key => $value) {
+                $fields[] = $key;
+                $values[] = $this->_write_db->quoteSmart($value);
+            }
+
+            $statement = OCIParse($this->_write_db->connection,
+                                  sprintf('INSERT INTO %s (%s, %s)' .
+                                          ' VALUES (%s, EMPTY_BLOB()) RETURNING %s INTO :blob',
+                                          $table,
+                                          implode(', ', $fields),
+                                          $field,
+                                          implode(', ', $values),
+                                          $field));
+
+            $lob = OCINewDescriptor($this->_write_db->connection);
+            OCIBindByName($statement, ':blob', $lob, -1, SQLT_BLOB);
+            OCIExecute($statement, OCI_DEFAULT);
+            $lob->save($data);
+            $result = OCICommit($this->_write_db->connection);
+            $lob->free();
+            OCIFreeStatement($statement);
+            return $result ? true : PEAR::raiseError('Unknown Error');
+
+        default:
+            foreach ($attributes as $key => $value) {
+                $fields[] = $key;
+                $values[] = $value;
+            }
+
+            $query = sprintf('INSERT INTO %s (%s, %s) VALUES (%s)',
+                             $table,
+                             implode(', ', $fields),
+                             $field,
+                             '?' . str_repeat(', ?', count($values)));
+            break;
+        }
+
+        switch ($this->_write_db->dbsyntax) {
+        case 'mssql':
+        case 'pgsql':
+            $values[] = bin2hex($data);
+            break;
+
+        default:
+            $values[] = $data;
+        }
+
+        /* Execute the query. */
+        $this->log($query, PEAR_LOG_DEBUG);
+        return $this->_write_db->query($query, $values);
+    }
+
+    /**
+     * TODO
+     *
+     * @access private
+     *
+     * @param string $table      TODO
+     * @param string $field      TODO
+     * @param string $data       TODO
+     * @param string $where      TODO
+     * @param array $alsoupdate  TODO
+     *
+     * @return mixed  TODO
+     */
+    function _updateBlob($table, $field, $data, $where, $alsoupdate)
+    {
+        $fields = array();
+        $values = array();
+
+        switch ($this->_write_db->dbsyntax) {
+        case 'oci8':
+            $wherestring = '';
+            foreach ($where as $key => $value) {
+                if (!empty($wherestring)) {
+                    $wherestring .= ' AND ';
+                }
+                $wherestring .= $key . ' = ' . $this->_write_db->quote($value);
+            }
+
+            $statement = OCIParse($this->_write_db->connection,
+                                  sprintf('SELECT %s FROM %s WHERE %s FOR UPDATE',
+                                          $field,
+                                          $table,
+                                          $wherestring));
+
+            OCIExecute($statement, OCI_DEFAULT);
+            OCIFetchInto($statement, $lob);
+            $lob[0]->save($data);
+            $result = OCICommit($this->_write_db->connection);
+            $lob[0]->free();
+            OCIFreeStatement($statement);
+            return $result ? true : PEAR::raiseError('Unknown Error');
+
+        default:
+            $updatestring = '';
+            $values = array();
+            foreach ($alsoupdate as $key => $value) {
+                $updatestring .= $key . ' = ?, ';
+                $values[] = $value;
+            }
+            $updatestring .= $field . ' = ?';
+            switch ($this->_write_db->dbsyntax) {
+            case 'mssql':
+            case 'pgsql':
+                $values[] = bin2hex($data);
+                break;
+
+            default:
+                $values[] = $data;
+            }
+
+            $wherestring = '';
+            foreach ($where as $key => $value) {
+                if (!empty($wherestring)) {
+                    $wherestring .= ' AND ';
+                }
+                $wherestring .= $key . ' = ?';
+                $values[] = $value;
+            }
+
+            $query = sprintf('UPDATE %s SET %s WHERE %s',
+                             $table,
+                             $updatestring,
+                             $wherestring);
+            break;
+        }
+
+        /* Execute the query. */
+        $this->log($query, PEAR_LOG_DEBUG);
+        return $this->_write_db->query($query, $values);
+    }
+
+    /**
+     * Converts the path name from regular filesystem form to the internal
+     * format needed to access the file in the database.
+     *
+     * Namely, we will treat '/' as a base directory as this is pretty much
+     * the standard way to access base directories over most filesystems.
+     *
+     * @access private
+     *
+     * @param string $path  A VFS path.
+     *
+     * @return string  The path with any surrouding slashes stripped off.
+     */
+    function _convertPath($path)
+    {
+        return trim($path, '/');
+    }
+
+    /**
+     * TODO
+     */
+    function _getFileSizeOp()
+    {
+        switch ($this->_db->dbsyntax) {
+        case 'mysql':
+            return 'LENGTH';
+
+        case 'oci8':
+            return 'LENGTHB';
+
+        case 'mssql':
+        case 'sybase':
+            return 'DATALENGTH';
+
+        case 'pgsql':
+        default:
+            return 'OCTET_LENGTH';
+        }
+    }
+
+    /**
+     * VFS_sql override of isFolder() to check for root folder.
+     *
+     * @param string $path  Path to possible folder
+     * @param string $name  Name of possible folder
+     *
+     * @return boolean        True if $path/$name is a folder
+     */
+    function isFolder($path, $name)
+    {
+        if ($path == '' && $name == '') {
+            // The root of VFS is always a folder.
+            return true;
+        }
+        return parent::isFolder($path, $name);
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/sql_file.php b/framework/VFS/lib/VFS/sql_file.php
new file mode 100644 (file)
index 0000000..0ee74d8
--- /dev/null
@@ -0,0 +1,845 @@
+<?php
+/**
+ * $Horde: framework/VFS/lib/VFS/sql_file.php,v 1.3 2009/10/15 17:13:28 jan Exp $
+ *
+ * @package VFS
+ */
+
+/**
+ * File value for vfs_type column.
+ */
+define('VFS_FILE', 1);
+
+/**
+ * Folder value for vfs_type column.
+ */
+define('VFS_FOLDER', 2);
+
+/**
+ * VFS_file parent class.
+ */
+include_once 'VFS/file.php';
+
+/**
+ * VFS:: implementation using PHP's PEAR database abstraction
+ * layer and local file system for file storage.
+ *
+ * Required values for $params:<pre>
+ *      'phptype'       The database type (ie. 'pgsql', 'mysql', etc.).
+ *      'vfsroot'       The root directory of where the files should be
+ *                      actually stored.</pre>
+ *
+ * Optional values:<pre>
+ *      'table'         The name of the vfs table in 'database'. Defaults to
+ *                      'horde_vfs'.</pre>
+ *
+ * Required by some database implementations:<pre>
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'database'      The name of the database.
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.</pre>
+ *
+ * The table structure for the VFS can be found in
+ * data/vfs.sql.
+ *
+ * $Horde: framework/VFS/lib/VFS/sql_file.php,v 1.3 2009/10/15 17:13:28 jan Exp $
+ *
+ * @author  Michael Varghese <mike.varghese@ascellatech.com>
+ * @package VFS
+ */
+class VFS_sql_file extends VFS_file {
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db = false;
+
+    /**
+     * Store a file in the VFS, with the data copied from a temporary
+     * file.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to be
+     *                             stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        /* No need to check quota here as we will check it when we call
+         * writeData(). */
+        $data = file_get_contents($tmpFile);
+        return $this->writeData($path, $name, $data, $autocreate);
+    }
+
+    /**
+     * Store a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $res = $this->_checkQuotaWrite('string', $data);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $fp = @fopen($this->_getNativePath($path, $name), 'w');
+        if (!$fp) {
+            if ($autocreate) {
+                $result = $this->autocreatePath($path);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                $fp = @fopen($this->_getNativePath($path, $name), 'w');
+                if (!$fp) {
+                    return PEAR::raiseError(_("Unable to open VFS file for writing."));
+                }
+            } else {
+                return PEAR::raiseError(_("Unable to open VFS file for writing."));
+            }
+        }
+
+        if (!@fwrite($fp, $data)) {
+            return PEAR::raiseError(_("Unable to write VFS file data."));
+        }
+
+        if (is_a($this->_writeSQLData($path, $name, $autocreate), 'PEAR_Error')) {
+            @unlink($this->_getNativePath($path, $name));
+            return PEAR::raiseError(_("Unable to write VFS file data."));
+        }
+    }
+
+    /**
+     * Moves a file in the database and the file system.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The old filename.
+     * @param string $dest         The new filename.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getNativePath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, false);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(_("Unable to move VFS file."));
+            }
+        }
+
+        if (strpos($dest, $this->_getSQLNativePath($path, $name)) !== false) {
+            return PEAR::raiseError(_("Unable to move VFS file."));
+        }
+
+        return $this->rename($path, $name, $dest, $name);
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $dest         The destination of the file.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getNativePath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($autocreate) {
+            $result = $this->autocreatePath($dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, false);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(_("Unable to copy VFS file."));
+            }
+        }
+
+        if (strpos($dest, $this->_getSQLNativePath($path, $name)) !== false) {
+            return PEAR::raiseError(_("Unable to copy VFS file."));
+        }
+
+        if (is_dir($orig)) {
+            return $this->_recursiveCopy($path, $name, $dest);
+        }
+
+        $res = $this->_checkQuotaWrite('file', $orig);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!@copy($orig, $this->_getNativePath($dest, $name))) {
+            return PEAR::raiseError(_("Unable to copy VFS file."));
+        }
+
+        $id = $this->_db->nextId($this->_params['table']);
+
+        $query = sprintf('INSERT INTO %s (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner) VALUES (?, ?, ?, ?, ?, ?)',
+                         $this->_params['table']);
+        $values = array($id, VFS_FILE, $dest, $name, time(), $this->_params['user']);
+
+        $result = $this->_db->query($query, $values);
+
+        if (is_a($result, 'PEAR_Error')) {
+            unlink($this->_getNativePath($dest, $name));
+            return $result;
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  Holds the path of directory to create folder.
+     * @param string $name  Holds the name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $id = $this->_db->nextId($this->_params['table']);
+        $result = $this->_db->query(sprintf('INSERT INTO %s (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner)
+                                            VALUES (?, ?, ?, ?, ?, ?)',
+                                            $this->_params['table']),
+                                    array($id, VFS_FOLDER, $path, $name, time(), $this->_params['user']));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!@mkdir($this->_getNativePath($path, $name))) {
+            $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_id = ?',
+                                                $this->_params['table']),
+                                        array($id));
+            return PEAR::raiseError(_("Unable to create VFS directory."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Rename a file or folder in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (strpos($newpath, '/') === false) {
+            $parent = '';
+            $path = $newpath;
+        } else {
+            list($parent, $path) = explode('/', $newpath, 2);
+        }
+        if (!$this->isFolder($parent, $path)) {
+            if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        $result = $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?, vfs_modified = ?
+                                            WHERE vfs_path = ? AND vfs_name = ?',
+                                            $this->_params['table']),
+                                    array($newpath, $newname, time(), $oldpath, $oldname));
+
+        if ($this->_db->affectedRows() == 0) {
+            return PEAR::raiseError(_("Unable to rename VFS file."));
+        }
+
+        if (is_a($this->_recursiveSQLRename($oldpath, $oldname, $newpath, $newname), 'PEAR_Error')) {
+            $result = $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?
+                                                WHERE vfs_path = ? AND vfs_name = ?',
+                                                $this->_params['table']),
+                                        array($oldpath, $oldname, $newpath, $newname));
+            return PEAR::raiseError(_("Unable to rename VFS directory."));
+        }
+
+        if (!@is_dir($this->_getNativePath($newpath))) {
+            if (is_a($res = $this->autocreatePath($newpath), 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        if (!@rename($this->_getNativePath($oldpath, $oldname), $this->_getNativePath($newpath, $newname))) {
+            $result = $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?
+                                                WHERE vfs_path = ? AND vfs_name = ?',
+                                                $this->_params['table']),
+                                        array($oldpath, $oldname, $newpath, $newname));
+            return PEAR::raiseError(_("Unable to rename VFS file."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Delete a folder from the VFS.
+     *
+     * @param string $path        The path to delete the folder from.
+     * @param string $name        The foldername to use.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($recursive) {
+            $result = $this->emptyFolder($path . '/' . $name);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $list = $this->listFolder($path . '/' . $name);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+            if (count($list)) {
+                return PEAR::raiseError(sprintf(_("Unable to delete %s, the directory is not empty"),
+                                                $path . '/' . $name));
+            }
+        }
+
+        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ? AND vfs_name = ?',
+                                            $this->_params['table']),
+                                    array(VFS_FOLDER, $path, $name));
+
+        if ($this->_db->affectedRows() == 0 || is_a($result, 'PEAR_Error')) {
+            return PEAR::raiseError(_("Unable to delete VFS directory."));
+        }
+
+        if (is_a($this->_recursiveSQLDelete($path, $name), 'PEAR_Error')) {
+            return PEAR::raiseError(_("Unable to delete VFS directory recursively."));
+        }
+
+        if (is_a($this->_recursiveLFSDelete($path, $name), 'PEAR_Error')) {
+            return PEAR::raiseError(_("Unable to delete VFS directory recursively."));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Delete a file from the VFS.
+     *
+     * @param string $path  The path to store the file in.
+     * @param string $name  The filename to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $res = $this->_checkQuotaDelete($path, $name);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ? AND vfs_name = ?',
+                                            $this->_params['table']),
+                                    array(VFS_FILE, $path, $name));
+
+        if ($this->_db->affectedRows() == 0) {
+            return PEAR::raiseError(_("Unable to delete VFS file."));
+        }
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!@unlink($this->_getNativePath($path, $name))) {
+            return PEAR::raiseError(_("Unable to delete VFS file."));
+        }
+
+        return true;
+    }
+
+    /**
+     * Return a list of the contents of a folder.
+     *
+     * @param string $path       The directory path.
+     * @param mixed $filter      String/hash of items to filter based on
+     *                           filename.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show directories only?
+     *
+     * @return mixed  File list on success or false on failure.
+     */
+    function _listFolder($path, $filter = null, $dotfiles = true,
+                        $dironly = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $files = array();
+        $fileList = array();
+
+        $fileList = $this->_db->getAll(sprintf('SELECT vfs_name, vfs_type, vfs_modified, vfs_owner FROM %s
+                                               WHERE vfs_path = ?',
+                                               $this->_params['table']),
+                                       array($path));
+        if (is_a($fileList, 'PEAR_Error')) {
+            return $fileList;
+        }
+
+        foreach ($fileList as $line) {
+            // Filter out dotfiles if they aren't wanted.
+            if (!$dotfiles && substr($line[0], 0, 1) == '.') {
+                continue;
+            }
+
+            $file['name'] = $line[0];
+
+            if ($line[1] == VFS_FILE) {
+                $name = explode('.', $line[0]);
+
+                if (count($name) == 1) {
+                    $file['type'] = '**none';
+                } else {
+                    $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                }
+
+                $file['size'] = filesize($this->_getNativePath($path, $line[0]));
+            } elseif ($line[1] == VFS_FOLDER) {
+                $file['type'] = '**dir';
+                $file['size'] = -1;
+            }
+
+            $file['date'] = $line[2];
+            $file['owner'] = $line[3];
+            $file['perms'] = '';
+            $file['group'] = '';
+
+            // Filtering.
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+        }
+
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        String/hash of items to filter based on
+     *                             folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = array(), $dotfolders = true)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $sql = sprintf('SELECT vfs_name, vfs_path FROM %s WHERE vfs_path = ? AND vfs_type = ?',
+                       $this->_params['table']);
+
+        $folderList = $this->_db->getAll($sql, array($path, $VFS_FOLDER));
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        $folders = array();
+        foreach ($folderList as $line) {
+            $folder['val'] = $this->_getNativePath($line[1], $line[0]);
+            $folder['abbrev'] = '';
+            $folder['label'] = '';
+
+            $count = substr_count($folder['val'], '/');
+
+            $x = 0;
+            while ($x < $count) {
+                $folder['abbrev'] .= '    ';
+                $folder['label'] .= '    ';
+                $x++;
+            }
+
+            $folder['abbrev'] .= $line[0];
+            $folder['label'] .= $line[0];
+
+            $strlen = VFS::strlen($folder['label']);
+            if ($strlen > 26) {
+                $folder['abbrev'] = substr($folder['label'], 0, ($count * 4));
+                $length = (29 - ($count * 4)) / 2;
+                $folder['abbrev'] .= substr($folder['label'], ($count * 4), $length);
+                $folder['abbrev'] .= '...';
+                $folder['abbrev'] .= substr($folder['label'], -1 * $length, $length);
+            }
+
+            $found = false;
+            foreach ($filter as $fltr) {
+                if ($folder['val'] == $fltr) {
+                    $found = true;
+                }
+            }
+
+            if (!$found) {
+                $folders[$folder['val']] = $folder;
+            }
+        }
+
+        ksort($folders);
+        return $folders;
+    }
+
+    /**
+     * Recursively copies the contents of a folder to a destination.
+     *
+     * @access private
+     *
+     * @param string $path  The path to store the directory in.
+     * @param string $name  The name of the directory.
+     * @param string $dest  The destination of the directory.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _recursiveCopy($path, $name, $dest)
+    {
+        $result = $this->createFolder($dest, $name);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $file_list = $this->listFolder($this->_getSQLNativePath($path, $name));
+        foreach ($file_list as $file) {
+            $result = $this->copy($this->_getSQLNativePath($path, $name), $file['name'], $this->_getSQLNativePath($dest, $name));
+
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        return true;
+     }
+
+    /**
+     * Store a files information within the database.
+     *
+     * @access private
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _writeSQLData($path, $name, $autocreate = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        // File already exists in database
+        if ($this->exists($path, $name)) {
+            $query = 'UPDATE ' . $this->_params['table'] .
+                     ' SET vfs_modified = ?' .
+                     ' WHERE vfs_path = ? AND vfs_name = ?';
+            $values = array(time(), $path, $name);
+        } else {
+            $id = $this->_db->nextId($this->_params['table']);
+
+            $query = 'INSERT INTO ' . $this->_params['table'] .
+                     ' (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified,' .
+                     ' vfs_owner) VALUES (?, ?, ?, ?, ?, ?)';
+            $values = array($id, VFS_FILE, $path, $name, time(),
+                            $this->_params['user']);
+        }
+        return $this->_db->query($query, $values);
+    }
+
+    /**
+     * Renames all child paths.
+     *
+     * @access private
+     *
+     * @param string $oldpath  The old path of the folder to rename.
+     * @param string $oldname  The old name.
+     * @param string $newpath  The new path of the folder to rename.
+     * @param string $newname  The new name.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _recursiveSQLRename($oldpath, $oldname, $newpath, $newname)
+    {
+        $folderList = $this->_db->getCol(sprintf('SELECT vfs_name FROM %s WHERE vfs_type = ? AND vfs_path = ?',
+                                                 $this->_params['table']),
+                                         0,
+                                         array(VFS_FOLDER, $this->_getSQLNativePath($oldpath, $oldname)));
+
+        foreach ($folderList as $folder) {
+            $this->_recursiveSQLRename($this->_getSQLNativePath($oldpath, $oldname), $folder, $this->_getSQLNativePath($newpath, $newname), $folder);
+        }
+
+        $result = $this->_db->query(sprintf('UPDATE %s SET vfs_path = ? WHERE vfs_path = ?',
+                                            $this->_params['table']),
+                                    array($this->_getSQLNativePath($newpath, $newname),
+                                          $this->_getSQLNativePath($oldpath, $oldname)));
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+    }
+
+    /**
+     * Delete a folders contents from the VFS in the SQL database,
+     * recursively.
+     *
+     * @access private
+     *
+     * @param string $path  The path of the folder.
+     * @param string $name  The foldername to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _recursiveSQLDelete($path, $name)
+    {
+        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ?',
+                                            $this->_params['table']),
+                                    array(VFS_FILE, $this->_getSQLNativePath($path, $name)));
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $folderList = $this->_db->getCol(sprintf('SELECT vfs_name FROM %s WHERE vfs_type = ? AND vfs_path = ?',
+                                                 $this->_params['table']),
+                                         0,
+                                         array(VFS_FOLDER, $this->_getSQLNativePath($path, $name)));
+
+        foreach ($folderList as $folder) {
+            $this->_recursiveSQLDelete($this->_getSQLNativePath($path, $name), $folder);
+        }
+
+        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_name = ? AND vfs_path = ?',
+                                            $this->_params['table']),
+                                    array(VFS_FOLDER, $name, $path));
+
+        return $result;
+    }
+
+    /**
+     * Delete a folders contents from the VFS, recursively.
+     *
+     * @access private
+     *
+     * @param string $path  The path of the folder.
+     * @param string $name  The foldername to use.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _recursiveLFSDelete($path, $name)
+    {
+        $dir = $this->_getNativePath($path, $name);
+        $dh = @opendir($dir);
+
+        while (false !== ($file = readdir($dh))) {
+            if ($file != '.' && $file != '..') {
+                if (is_dir($dir . '/' . $file)) {
+                    $this->_recursiveLFSDelete(!strlen($path) ? $name : $path . '/' . $name, $file);
+                } else {
+                    @unlink($dir . '/' . $file);
+                }
+            }
+        }
+        @closedir($dh);
+
+        return rmdir($dir);
+    }
+
+    /**
+     * Attempts to open a persistent connection to the SQL server.
+     *
+     * @access private
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _connect()
+    {
+        if ($this->_db !== false) {
+            return true;
+        }
+
+        if (!is_array($this->_params)) {
+            return PEAR::raiseError(_("No configuration information specified for SQL-File VFS."));
+        }
+
+        $required = array('phptype', 'vfsroot');
+        foreach ($required as $val) {
+            if (!isset($this->_params[$val])) {
+                return PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), $val));
+            }
+        }
+
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+
+        if (!isset($this->_params['table'])) {
+            $this->_params['table'] = 'horde_vfs';
+        }
+
+        /* Connect to the SQL server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_db = &DB::connect($this->_params,
+                                  array('persistent' => !empty($this->_params['persistent']),
+                                        'ssl' => !empty($this->_params['ssl'])));
+        if (is_a($this->_db, 'PEAR_Error')) {
+            $error = $this->_db;
+            $this->_db = false;
+            return $error;
+        }
+
+        // Set DB portability options.
+        switch ($this->_db->phptype) {
+        case 'mssql':
+            $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+
+        default:
+            $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+        }
+
+        return true;
+    }
+
+    /**
+     * Return a full filename on the native filesystem, from a VFS
+     * path and name.
+     *
+     * @access private
+     *
+     * @param string $path  The VFS file path.
+     * @param string $name  The VFS filename.
+     *
+     * @return string  The full native filename.
+     */
+    function _getNativePath($path, $name)
+    {
+        if (strlen($name)) {
+            $name = '/' . $name;
+        }
+        if (strlen($path)) {
+            if (isset($this->_params['home']) &&
+                preg_match('|^~/?(.*)$|', $path, $matches)) {
+                $path = $this->_params['home']  . '/' . $matches[1];
+            }
+
+            return $this->_params['vfsroot'] . '/' . $path . $name;
+        } else {
+            return $this->_params['vfsroot'] . $name;
+        }
+    }
+
+    /**
+     * Return a full SQL filename on the native filesystem, from a VFS
+     * path and name.
+     *
+     * @access private
+     *
+     * @param string $path  The VFS file path.
+     * @param string $name  The VFS filename.
+     *
+     * @return string  The full native filename.
+     */
+    function _getSQLNativePath($path, $name)
+    {
+        if (!strlen($path)) {
+            return $name;
+        }
+
+        return $path . '/' . $name;
+    }
+
+}
diff --git a/framework/VFS/lib/VFS/ssh2.php b/framework/VFS/lib/VFS/ssh2.php
new file mode 100644 (file)
index 0000000..45c69a4
--- /dev/null
@@ -0,0 +1,1006 @@
+<?php
+/**
+ * VFS implementation for an SSH2 server.
+ * This module requires the SSH2 (version 0.10+) PECL package.
+ *
+ * Required values for $params:<pre>
+ *      'username'       The username with which to connect to the ssh2 server.
+ *      'password'       The password with which to connect to the ssh2 server.
+ *      'hostspec'       The ssh2 server to connect to.</pre>
+ *
+ * Optional values for $params:<pre>
+ *      'port'           The port used to connect to the ssh2 server if other
+ *                       than 22.</pre>
+ *
+ * $Horde: framework/VFS/lib/VFS/ssh2.php,v 1.16 2009-11-01 02:28:54 chuck Exp $
+ *
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @editor  Cliff Green <green@umdnj.edu>
+ * @since   Horde 3.2
+ * @package VFS
+ */
+class VFS_ssh2 extends VFS {
+
+    /**
+     * List of additional credentials required for this VFS backend.
+     *
+     * @var array
+     */
+    var $_credentials = array('username', 'password');
+
+    /**
+     * List of permissions and if they can be changed in this VFS backend.
+     *
+     * @var array
+     */
+    var $_permissions = array(
+        'owner' => array('read' => true, 'write' => true, 'execute' => true),
+        'group' => array('read' => true, 'write' => true, 'execute' => true),
+        'all'   => array('read' => true, 'write' => true, 'execute' => true));
+
+    /**
+     * Variable holding the connection to the ssh2 server.
+     *
+     * @var resource
+     */
+    var $_stream = false;
+
+    /**
+     * The SFTP resource stream.
+     *
+     * @var resource
+     */
+    var $_sftp;
+
+    /**
+     * The current working directory.
+     *
+     * @var string
+     */
+    var $_cwd;
+
+    /**
+     * Local cache array for user IDs.
+     *
+     * @var array
+     */
+    var $_uids = array();
+
+    /**
+     * Local cache array for group IDs.
+     *
+     * @var array
+     */
+    var $_gids = array();
+
+    /**
+     * Returns the size of a file.
+     *
+     * @access public
+     *
+     * @param string $path  The path of the file.
+     * @param string $name  The filename.
+     *
+     * @return integer  The size of the file in bytes or PEAR_Error on
+     *                  failure.
+     */
+    function size($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $statinfo = @ssh2_sftp_stat($this->_sftp, $this->_getPath($path, $name));
+        if (($size = $statinfo['size']) === false) {
+            return PEAR::raiseError(sprintf(_("Unable to check file size of \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return $size;
+    }
+
+    /**
+     * Retrieves a file from the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string  The file data.
+     */
+    function read($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        clearstatcache();
+        $size = filesize($file);
+        if ($size === 0) {
+            return '';
+        }
+
+        return file_get_contents($file);
+    }
+
+    /**
+     * Retrieves a file from the VFS as an on-disk local file.
+     *
+     * This function provides a file on local disk with the data of a VFS file
+     * in it. This file <em>cannot</em> be modified! The behavior if you do
+     * modify it is undefined. It will be removed at the end of the request.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return string A local filename.
+     */
+    function readFile($path, $name)
+    {
+        $result = $this->_connect();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Create a temporary file and register it for deletion at the
+        // end of this request.
+        $localFile = $this->_getTempFile();
+        if (!$localFile) {
+            return PEAR::raiseError(_("Unable to create temporary file."));
+        }
+        register_shutdown_function(create_function('', 'unlink(\'' . addslashes($localFile) . '\');'));
+
+        if (!$this->_recv($this->_getPath($path, $name), $localFile)) {
+            return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return $localFile;
+    }
+
+    /**
+     * Open a stream to a file in the VFS.
+     *
+     * @param string $path  The pathname to the file.
+     * @param string $name  The filename to retrieve.
+     *
+     * @return resource  The stream.
+     */
+    function readStream($path, $name)
+    {
+        $file = $this->readFile($path, $name);
+        if (is_a($file, 'PEAR_Error')) {
+            return $file;
+        }
+
+        $mode = OS_WINDOWS ? 'rb' : 'r';
+        return fopen($file, $mode);
+    }
+
+    /**
+     * Stores a file in the VFS.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $tmpFile      The temporary file containing the data to
+     *                             be stored.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function write($path, $name, $tmpFile, $autocreate = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $res = $this->_checkQuotaWrite('file', $tmpFile);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!$this->_send($tmpFile, $this->_getPath($path, $name)))  {
+            if ($autocreate) {
+                $result = $this->autocreatePath($path);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+                if (!$this->_send($tmpFile, $this->_getPath($path, $name))) {
+                    return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
+                }
+            } else {
+                return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Stores a file in the VFS from raw data.
+     *
+     * @param string $path         The path to store the file in.
+     * @param string $name         The filename to use.
+     * @param string $data         The file data.
+     * @param boolean $autocreate  Automatically create directories?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function writeData($path, $name, $data, $autocreate = false)
+    {
+        $res = $this->_checkQuotaWrite('string', $data);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $tmpFile = $this->_getTempFile();
+        if (function_exists('file_put_contents')) {
+            file_put_contents($tmpFile, $data);
+        } else {
+            $fp = fopen($tmpFile, 'wb');
+            fwrite($fp, $data);
+            fclose($fp);
+        }
+
+        $result = $this->write($path, $name, $tmpFile, $autocreate);
+        unlink($tmpFile);
+        return $result;
+    }
+
+    /**
+     * Deletes a file from the VFS.
+     *
+     * @param string $path  The path to delete the file from.
+     * @param string $name  The filename to delete.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFile($path, $name)
+    {
+        $res = $this->_checkQuotaDelete($path, $name);
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ssh2_sftp_unlink($this->_sftp, $this->_getPath($path, $name))) {
+            return PEAR::raiseError(sprintf(_("Unable to delete VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if a given item is a folder.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The item name.
+     *
+     * @return boolean  True if it is a folder, false otherwise.
+     */
+    function isFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        /* See if we can stat the remote filename. ANDed with 040000 is true
+         * if it is a directory. */
+        $statinfo = @ssh2_sftp_stat($this->_sftp, $this->_getPath($path, $name));
+        return $statinfo['mode'] & 040000;
+    }
+
+    /**
+     * Deletes a folder from the VFS.
+     *
+     * @param string $path        The parent folder.
+     * @param string $name        The name of the folder to delete.
+     * @param boolean $recursive  Force a recursive delete?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function deleteFolder($path, $name, $recursive = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $isDir = false;
+        $dirCheck = $this->listFolder($path);
+        foreach ($dirCheck as $file) {
+            if ($file['name'] == $name && $file['type'] == '**dir') {
+                $isDir = true;
+                break;
+            }
+        }
+
+        if ($isDir) {
+            $file_list = $this->listFolder($this->_getPath($path, $name));
+            if (is_a($file_list, 'PEAR_Error')) {
+                return $file_list;
+            }
+
+            if (count($file_list) && !$recursive) {
+                return PEAR::raiseError(sprintf(_("Unable to delete \"%s\", the directory is not empty."),
+                                                $this->_getPath($path, $name)));
+            }
+            foreach ($file_list as $file) {
+                if ($file['type'] == '**dir') {
+                    $result = $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
+                } else {
+                    $result = $this->deleteFile($this->_getPath($path, $name), $file['name']);
+                }
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            if (!@ssh2_sftp_rmdir($this->_sftp, $this->_getPath($path, $name))) {
+                return PEAR::raiseError(sprintf(_("Cannot remove directory \"%s\"."), $this->_getPath($path, $name)));
+            }
+        } else {
+            if (!@ssh2_sftp_unlink($this->_sftp, $this->_getPath($path, $name))) {
+                return PEAR::raiseError(sprintf(_("Cannot delete file \"%s\"."), $this->_getPath($path, $name)));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Renames a file in the VFS.
+     *
+     * @param string $oldpath  The old path to the file.
+     * @param string $oldname  The old filename.
+     * @param string $newpath  The new path of the file.
+     * @param string $newname  The new filename.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function rename($oldpath, $oldname, $newpath, $newname)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (is_a($res = $this->autocreatePath($newpath), 'PEAR_Error')) {
+            return $res;
+        }
+
+        if (!@ssh2_sftp_rename($this->_sftp, $this->_getPath($oldpath, $oldname), $this->_getPath($newpath, $newname))) {
+            return PEAR::raiseError(sprintf(_("Unable to rename VFS file \"%s\"."), $this->_getPath($oldpath, $oldname)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a folder on the VFS.
+     *
+     * @param string $path  The parent folder.
+     * @param string $name  The name of the new folder.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function createFolder($path, $name)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ssh2_sftp_mkdir($this->_sftp, $this->_getPath($path, $name))) {
+            return PEAR::raiseError(sprintf(_("Unable to create VFS directory \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Changes permissions for an item on the VFS.
+     *
+     * @param string $path        The parent folder of the item.
+     * @param string $name        The name of the item.
+     * @param string $permission  The permission to set.
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function changePermissions($path, $name, $permission)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if (!@ssh2_exec($this->_stream, 'chmod ' . escapeshellarg($permission) . ' ' . escapeshellarg($this->_getPath($path, $name)))) {
+            return PEAR::raiseError(sprintf(_("Unable to change permission for VFS file \"%s\"."), $this->_getPath($path, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns an an unsorted file list of the specified directory.
+     *
+     * @param string $path       The path of the directory.
+     * @param mixed $filter      String/hash to filter file/dirname on.
+     * @param boolean $dotfiles  Show dotfiles?
+     * @param boolean $dironly   Show only directories?
+     *
+     * @return array  File list on success or PEAR_Error on failure.
+     */
+    function _listFolder($path = '', $filter = null, $dotfiles = true,
+                         $dironly = false)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $files = array();
+
+        /* If 'maplocalids' is set, check for the POSIX extension. */
+        $mapids = false;
+        if (!empty($this->_params['maplocalids']) &&
+            extension_loaded('posix')) {
+            $mapids = true;
+        }
+
+        // THIS IS A PROBLEM....  there is no builtin systype() fn for SSH2.
+        // Go with unix-style listings for now...
+        $type = 'unix';
+
+        $olddir = $this->getCurrentDirectory();
+        if (strlen($path)) {
+            $res = $this->_setPath($path);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        if ($type == 'unix') {
+            $ls_args = 'l';
+
+            // Get numeric ids if we're going to use posix_* functions to
+            // map them.
+            if ($mapids) {
+                $ls_args .= 'n';
+            }
+
+            // If we don't want dotfiles, We can save work here by not doing
+            // an ls -a and then not doing the check later (by setting
+            // $dotfiles to true, the if is short-circuited).
+            if ($dotfiles) {
+                $ls_args .= 'a';
+                $dotfiles = true;
+            }
+
+            $stream = @ssh2_exec($this->_stream, 'LC_TIME=C ls -' . $ls_args . ' ' . escapeshellarg($path));
+        } else {
+            $stream = @ssh2_exec($this->_stream, '');
+        }
+
+        stream_set_blocking($stream, true);
+        unset($list);
+        while (!feof($stream)) {
+            $line = fgets($stream);
+            if ($line === false) {
+                break;
+            }
+            $list[] = trim($line);
+        }
+        fclose($stream);
+
+        if (!is_array($list)) {
+            if (isset($olddir)) {
+                $res = $this->_setPath($olddir);
+                if (is_a($res, 'PEAR_Error')) {
+                    return $res;
+                }
+            }
+            return array();
+        }
+
+        $currtime = time();
+
+        foreach ($list as $line) {
+            $file = array();
+            $item = preg_split('/\s+/', $line);
+            if (($type == 'unix') ||
+                (($type == 'win') &&
+                 !preg_match('|\d\d-\d\d-\d\d|', $item[0]))) {
+                if ((count($item) < 8) || (substr($line, 0, 5) == 'total')) {
+                    continue;
+                }
+                $file['perms'] = $item[0];
+                if ($mapids) {
+                    if (!isset($this->_uids[$item[2]])) {
+                        $entry = posix_getpwuid($item[2]);
+                        $this->_uids[$item[2]] = (empty($entry)) ? $item[2] : $entry['name'];
+                    }
+                    $file['owner'] = $this->_uids[$item[2]];
+                    if (!isset($this->_uids[$item[3]])) {
+                        $entry = posix_getgrgid($item[3]);
+                        $this->_uids[$item[3]] = (empty($entry)) ? $item[3] : $entry['name'];
+                    }
+                    $file['group'] = $this->_uids[$item[3]];
+
+                } else {
+                    $file['owner'] = $item[2];
+                    $file['group'] = $item[3];
+                }
+                $file['name'] = substr($line, strpos($line, sprintf("%s %2s %5s", $item[5], $item[6], $item[7])) + 13);
+
+                // Filter out '.' and '..' entries.
+                if (preg_match('/^\.\.?\/?$/', $file['name'])) {
+                    continue;
+                }
+
+                // Filter out dotfiles if they aren't wanted.
+                if (!$dotfiles && (substr($file['name'], 0, 1) == '.')) {
+                    continue;
+                }
+
+                $p1 = substr($file['perms'], 0, 1);
+                if ($p1 === 'l') {
+                    $file['link'] = substr($file['name'], strpos($file['name'], '->') + 3);
+                    $file['name'] = substr($file['name'], 0, strpos($file['name'], '->') - 1);
+                    $file['type'] = '**sym';
+
+                   if ($this->isFolder('', $file['link'])) {
+                       $file['linktype'] = '**dir';
+                   } else {
+                       $parts = explode('/', $file['link']);
+                       $name = explode('.', array_pop($parts));
+                       if ((count($name) == 1) ||
+                           (($name[0] === '') && (count($name) == 2))) {
+                           $file['linktype'] = '**none';
+                       } else {
+                           $file['linktype'] = VFS::strtolower(array_pop($name));
+                       }
+                   }
+                } elseif ($p1 === 'd') {
+                    $file['type'] = '**dir';
+                } else {
+                    $name = explode('.', $file['name']);
+                    if ((count($name) == 1) ||
+                        ((substr($file['name'], 0, 1) === '.') &&
+                         (count($name) == 2))) {
+                        $file['type'] = '**none';
+                    } else {
+                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                    }
+                }
+                if ($file['type'] == '**dir') {
+                    $file['size'] = -1;
+                } else {
+                    $file['size'] = $item[4];
+                }
+                if (strpos($item[7], ':') !== false) {
+                    $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . date('Y', $currtime));
+                    // If the ssh2 server reports a file modification date more
+                    // less than one day in the future, don't try to subtract
+                    // a year from the date.  There is no way to know, for
+                    // example, if the VFS server and the ssh2 server reside
+                    // in different timezones.  We should simply report to the
+                    //  user what the SSH2 server is returning.
+                    if ($file['date'] > ($currtime + 86400)) {
+                        $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . (date('Y', $currtime) - 1));
+                    }
+                } else {
+                    $file['date'] = strtotime('00:00:00' . $item[5] . ' ' . $item[6] . ' ' . $item[7]);
+                }
+            } elseif ($type == 'netware') {
+                $file = Array();
+                $file['perms'] = $item[1];
+                $file['owner'] = $item[2];
+                if ($item[0] == 'd') {
+                    $file['type'] = '**dir';
+                } else {
+                    $file['type'] = '**none';
+                }
+                $file['size'] = $item[3];
+                $file['name'] = $item[7];
+                $index = 8;
+                while ($index < count($item)) {
+                    $file['name'] .= ' ' . $item[$index];
+                    $index++;
+                }
+            } else {
+                /* Handle Windows SSH2 servers returning DOS-style file
+                 * listings. */
+                $file['perms'] = '';
+                $file['owner'] = '';
+                $file['group'] = '';
+                $file['name'] = $item[3];
+                $index = 4;
+                while ($index < count($item)) {
+                    $file['name'] .= ' ' . $item[$index];
+                    $index++;
+                }
+                $file['date'] = strtotime($item[0] . ' ' . $item[1]);
+                if ($item[2] == '<DIR>') {
+                    $file['type'] = '**dir';
+                    $file['size'] = -1;
+                } else {
+                    $file['size'] = $item[2];
+                    $name = explode('.', $file['name']);
+                    if ((count($name) == 1) ||
+                        ((substr($file['name'], 0, 1) === '.') &&
+                         (count($name) == 2))) {
+                        $file['type'] = '**none';
+                    } else {
+                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
+                    }
+                }
+            }
+
+            // Filtering.
+            if ($this->_filterMatch($filter, $file['name'])) {
+                unset($file);
+                continue;
+            }
+            if ($dironly && $file['type'] !== '**dir') {
+                unset($file);
+                continue;
+            }
+
+            $files[$file['name']] = $file;
+            unset($file);
+        }
+
+        if (isset($olddir)) {
+            $res = $this->_setPath($olddir);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        return $files;
+    }
+
+    /**
+     * Returns a sorted list of folders in the specified directory.
+     *
+     * @param string $path         The path of the directory to get the
+     *                             directory list for.
+     * @param mixed $filter        Hash of items to filter based on folderlist.
+     * @param boolean $dotfolders  Include dotfolders?
+     *
+     * @return mixed  Folder list on success or a PEAR_Error object on failure.
+     */
+    function listFolders($path = '', $filter = null, $dotfolders = true)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $folders = array();
+        $folder = array();
+
+        $folderList = $this->listFolder($path, null, $dotfolders, true);
+        if (is_a($folderList, 'PEAR_Error')) {
+            return $folderList;
+        }
+
+        $folder['val'] = $this->_parentDir($path);
+        $folder['abbrev'] = $folder['label'] = '..';
+
+        $folders[$folder['val']] = $folder;
+
+        foreach ($folderList as $files) {
+            $folder['val'] = $this->_getPath($path, $files['name']);
+            $folder['abbrev'] = $files['name'];
+            $folder['label'] = $folder['val'];
+
+            $folders[$folder['val']] = $folder;
+        }
+
+        ksort($folders);
+        return $folders;
+    }
+
+    /**
+     * Copies a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The name of the destination directory.
+     * @param boolean $autocreate  Auto-create the directory if it doesn't
+     *                             exist?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function copy($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($autocreate) {
+            $res = $this->autocreatePath($dest);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
+            }
+        }
+
+        if ($this->isFolder($path, $name)) {
+            $result = $this->_copyRecursive($path, $name, $dest);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        } else {
+            $tmpFile = $this->_getTempFile();
+            $fetch = $this->_recv($orig, $tmpFile);
+            if (!$fetch) {
+                unlink($tmpFile);
+                return PEAR::raiseError(sprintf(_("Failed to copy from \"%s\"."), $orig));
+            }
+
+            $res = $this->_checkQuotaWrite('file', $tmpFile);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+
+            if (!$this->_send($tmpFile, $this->_getPath($dest, $name))) {
+                unlink($tmpFile);
+                return PEAR::raiseError(sprintf(_("Failed to copy to \"%s\"."), $this->_getPath($dest, $name)));
+            }
+
+            unlink($tmpFile);
+        }
+
+        return true;
+    }
+
+    /**
+     * Moves a file through the backend.
+     *
+     * @param string $path         The path of the original file.
+     * @param string $name         The name of the original file.
+     * @param string $dest         The destination file name.
+     * @param boolean $autocreate  Auto-create the directory if it doesn't
+     *                             exist?
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function move($path, $name, $dest, $autocreate = false)
+    {
+        $orig = $this->_getPath($path, $name);
+        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
+            return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
+        }
+
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        if ($autocreate) {
+            $res = $this->autocreatePath($dest);
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        $fileCheck = $this->listFolder($dest, null, true);
+        if (is_a($fileCheck, 'PEAR_Error')) {
+            return $fileCheck;
+        }
+
+        foreach ($fileCheck as $file) {
+            if ($file['name'] == $name) {
+                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
+            }
+        }
+
+        if (!@ssh2_sftp_rename($this->_sftp, $orig, $this->_getPath($dest, $name))) {
+            return PEAR::raiseError(sprintf(_("Failed to move to \"%s\"."), $this->_getPath($dest, $name)));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the current working directory on the SSH2 server.
+     *
+     * @return string  The current working directory.
+     */
+    function getCurrentDirectory()
+    {
+        if (is_a($res = $this->_connect(), 'PEAR_Error')) {
+            return $res;
+        }
+        if (!strlen($this->_cwd)) {
+            $stream = @ssh2_exec($this->_stream, 'pwd');
+            stream_set_blocking($stream, true);
+            $this->_cwd = trim(fgets($stream));
+        }
+        return $this->_cwd;
+    }
+
+    /**
+     * Changes the current directory on the server.
+     *
+     * @access private
+     *
+     * @param string $path  The path to change to.
+     *
+     * @return boolean  True on success, or a PEAR_Error on failure.
+     */
+    function _setPath($path)
+    {
+        if ($stream = @ssh2_exec($this->_stream, 'cd ' . escapeshellarg($path) . '; pwd')) {
+            stream_set_blocking($stream, true);
+            $this->_cwd = trim(fgets($stream));
+            fclose($stream);
+            return true;
+        } else {
+            return PEAR::raiseError(sprintf(_("Unable to change to %s."), $path));
+        }
+    }
+
+    /**
+     * Returns the full path of an item.
+     *
+     * @access private
+     *
+     * @param string $path  The directory of the item.
+     * @param string $name  The name of the item.
+     *
+     * @return mixed  Full path to the file when $path is not empty and just
+     *                $name when not set.
+     */
+    function _getPath($path, $name)
+    {
+        if ($path !== '') {
+            return ($path . '/' . $name);
+        }
+        return $name;
+    }
+
+    /**
+     * Returns the parent directory of the specified path.
+     *
+     * @access private
+     *
+     * @param string $path  The path to get the parent of.
+     *
+     * @return string  The parent directory (string) on success or a PEAR_Error
+     *                 object on failure.
+     */
+    function _parentDir($path)
+    {
+        $conn = $this->_connect();
+        if (is_a($conn, 'PEAR_Error')) {
+            return $conn;
+        }
+
+        $this->_setPath('cd ' . $path . '/..');
+        return $this->getCurrentDirectory();
+    }
+
+    /**
+     * Attempts to open a connection to the SSH2 server.
+     *
+     * @access private
+     *
+     * @return mixed  True on success or a PEAR_Error object on failure.
+     */
+    function _connect()
+    {
+        if ($this->_stream === false) {
+            if (!extension_loaded('ssh2')) {
+                return PEAR::raiseError(_("The SSH2 PECL extension is not available."));
+            }
+
+            if (!is_array($this->_params)) {
+                return PEAR::raiseError(_("No configuration information specified for SSH2 VFS."));
+            }
+
+            $required = array('hostspec', 'username', 'password');
+            foreach ($required as $val) {
+                if (!isset($this->_params[$val])) {
+                    return PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), $val));
+                }
+            }
+
+            /* Connect to the ssh2 server using the supplied parameters. */
+            if (empty($this->_params['port'])) {
+                $this->_stream = @ssh2_connect($this->_params['hostspec']);
+            } else {
+                $this->_stream = @ssh2_connect($this->_params['hostspec'], $this->_params['port']);
+            }
+            if (!$this->_stream) {
+                return PEAR::raiseError(_("Connection to SSH2 server failed."));
+            }
+
+            $connected = @ssh2_auth_password($this->_stream, $this->_params['username'], $this->_params['password']);
+            if (!$connected) {
+                $this->_stream = false;
+                return PEAR::raiseError(_("Authentication to SSH2 server failed."));
+            }
+
+            /* Create sftp resource. */
+            $this->_sftp = @ssh2_sftp($this->_stream);
+        }
+
+        return true;
+    }
+
+    /**
+     * Sends local file to remote host.
+     * This function exists because the php_scp_* functions doesn't seem to work on some hosts.
+     *
+     * @access private
+     *
+     * @param string $local   Full path to the local file.
+     * @param string $remote  Full path to the remote location.
+     *
+     * @return boolean TRUE on success, FALSE on failure.
+     */
+    function _send($local, $remote)
+    {
+        return @copy($local, $this->_wrap($remote));
+    }
+
+    /**
+     * Receives file from remote host.
+     * This function exists because the php_scp_* functions doesn't seem to work on some hosts.
+     *
+     * @access private
+     *
+     * @param string $local  Full path to the local file.
+     * @param string $remote Full path to the remote location.
+     *
+     * @return boolean TRUE on success, FALSE on failure.
+     */
+    function _recv($remote, $local)
+    {
+        return @copy($this->_wrap($remote), $local);
+    }
+
+    /**
+     * Generate a stream wrapper file spec for a remote file path
+     *
+     * @access private
+     *
+     * @param string $remote  Full path to the remote location
+     *
+     * @return string  A full stream wrapper path to the remote location
+     */
+    function _wrap($remote)
+    {
+        return 'ssh2.sftp://' . $this->_params['username'] . ':' . $this->_params['password']
+            . '@' . $this->_params['hostspec'] . ':' . $this->_params['port'] . $remote;
+    }
+
+}
diff --git a/framework/VFS/package.xml b/framework/VFS/package.xml
new file mode 100644 (file)
index 0000000..7ac7d4b
--- /dev/null
@@ -0,0 +1,283 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>VFS</name>
+ <channel>pear.php.net</channel>
+ <summary>Virtual File System API</summary>
+ <description>This package provides a Virtual File System API, with backends for:
+
+* SQL
+* FTP
+* Local filesystems
+* Hybrid SQL and filesystem
+* Samba
+* SSH2/SFTP
+* IMAP (Kolab)
+
+Reading, writing and listing of files are all supported, and there are both object-based and array-based interfaces to directory listings.
+
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chagenbu</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>yunosh</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-12-31</date>
+ <version>
+  <release>0.4.0</release>
+  <api>0.3.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes></notes>
+ <contents>
+  <dir name="/">
+   <dir name="data">
+    <dir name="VFS">
+     <file name="muvfs.sql" role="data" />
+     <file name="vfs.oci8.sql" role="data" />
+     <file name="vfs.pgsql.sql" role="data" />
+     <file name="vfs.sql" role="data" />
+    </dir> <!-- /data/VFS -->
+   </dir> <!-- /data -->
+   <dir name="scripts">
+    <dir name="VFS">
+     <file name="vfs.php" role="script">
+      <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+     </file>
+    </dir> <!-- /scripts/VFS -->
+   </dir> <!-- /scripts -->
+   <dir name="lib">
+    <dir name="VFS">
+     <file name="Browser.php" role="php" />
+     <file name="file.php" role="php" />
+     <file name="ftp.php" role="php" />
+     <file name="GC.php" role="php" />
+     <file name="horde.php" role="php" />
+     <file name="kolab.php" role="php" />
+     <file name="ListItem.php" role="php" />
+     <file name="musql.php" role="php" />
+     <file name="Object.php" role="php" />
+     <file name="smb.php" role="php" />
+     <file name="sql.php" role="php" />
+     <file name="sql_file.php" role="php" />
+     <file name="ssh2.php" role="php" />
+    </dir> <!-- /lib/VFS -->
+    <file name="VFS.php" role="php" />
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Log</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="scripts/VFS/vfs.php" as="vfs" />
+   <install name="lib/VFS/Browser.php" as="VFS/Browser.php" />
+   <install name="lib/VFS/file.php" as="VFS/file.php" />
+   <install name="lib/VFS/ftp.php" as="VFS/ftp.php" />
+   <install name="lib/VFS/GC.php" as="VFS/GC.php" />
+   <install name="lib/VFS/horde.php" as="VFS/horde.php" />
+   <install name="lib/VFS/ListItem.php" as="VFS/ListItem.php" />
+   <install name="lib/VFS/musql.php" as="VFS/musql.php" />
+   <install name="lib/VFS/Object.php" as="VFS/Object.php" />
+   <install name="lib/VFS/smb.php" as="VFS/smb.php" />
+   <install name="lib/VFS/sql.php" as="VFS/sql.php" />
+   <install name="lib/VFS/sql_file.php" as="VFS/sql_file.php" />
+   <install name="lib/VFS/ssh2.php" as="VFS/ssh2.php" />
+   <install name="lib/VFS.php" as="VFS.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2009-10-15</date>
+   <version>
+    <release>0.3.0</release>
+    <api>0.3.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Rename fixes for SMB driver (leena.heino@uta.fi, Horde Bug #7933).
+* Set LC_TIME to C for consistency in date parsing in the SSH2 backend (michael-dev@fami-braun.de, Horde Bug #6396)
+* Pass -n for numeric UIDs/GIDs in the SSH2 backend when mapping to local user ids (Horde Request #6579)
+* Fix readByteRange() (wrong method name) in SQL backend
+* Add readFile() support, with optimizations for file, ftp, ssh2, and smb backends
+* Add readStream() support to backends that can get a stream resource efficiently
+* Add delete() as a shorter name for deleteFile()
+* Add a Kolab VFS driver.
+* Use streams for file transfers in VFS_ssh2 (jives@jives.ixum.net, Horde Request #8323)
+* Check quota when copying files (Horde Bug #8643).</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.2.0</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2008-02-16</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Fix listing of folders with whitespace in the name using the SSH2 VFS driver (Horde Bug #6227).
+* Fix some string parsing on systems that don't have the 'en' or 'en_US' locales installed (Horde Bug #6194).
+* PHP 4.3.0+ is now required and we make unconditional use of file_get_contents().
+* Use strlen() instead of empty() to check for paths and filenames being set, to allow for potential names of 0 (zero).
+* Converted to package.xml 2.0.
+* Fix saving files in the root directory of an SQL backend.
+* Never generate paths with double slashes in them (//) (Horde Bug #5821)
+* Improve Netware FTP support (Horde Bug #5821)
+* Add read stream support to some backends (Horde Bug #5913)
+* Make the smb backend compatible with samba versions after samba-3.0.25-pre2 (felix.leimbach@gmx.net, Horde Bug #5931)</notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2006-04-12</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* SQL drivers now return empty values if the particular parameter does not exist on the server (instead of &apos;-&apos;).
+* Add initial VFS quota support.
+* Fix autocreatePath() to work with directory paths that begin with the base directory.
+* Add &apos;maplocalids&apos; and &apos;timeout&apos; parameters to the FTP driver.
+* In SQL driver, allow &apos;/&apos; to indicate the base directory.
+* Fix return value of deleteFile() in File and SQLFile drivers (PEAR Bug #4425).
+* Fix listFolders() in File driver (PEAR Bug #4533).
+* Add size() and readByteRange(), with File and SQL implementations.
+* Add support for NetWare FTP server listings (Lukas Macura &lt;macura@opf.slu.cz&gt;).
+* Add support for separate read and write DB servers for the SQL driver.
+* Add copy() and move() fallback implementations for all drivers.
+* Add parameters to copy() and move() to automatically create destination directories.
+* Add SSH2/SFTP driver (Cliff Green &lt;green@umdnj.edu&gt;).
+* Let rename() automatically create the destination path.
+* Make sure copy/move doesn&apos;t result in infinite recursion (Horde Bug #3680).
+      
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.5</release>
+    <api>0.0.5</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-04-07</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Fix typo that prevented the sql_file driver to load its parameters.
+* PHP 5 compatibility fixes.
+* Add &apos;pasv&apos; parameter for FTP driver.
+* Add writeData() method to sql_file driver.
+* Support ~/path when a &apos;home&apos; parameter is defined.
+* Add a CLI script for accessing and modifying VFS backends.
+* Added getCurrentDirectory() method.
+* phpdoc has been updated and should now generate correctly in all files.
+* Return error if trying to copy a file onto itself with the file driver (http://bugs.horde.org/ticket/?id=168).
+* Several bug fixes to listFolders() method in SQL drivers.
+* A PEAR Log object can now be specified for logging messages to.
+* Added getParam() method.
+* Allow recursive listings in listFolder().
+* Several fixes with zero length files.
+* Fixed autocreating of directories and updating of existing files in the sql_file VFS driver (http://bugs.horde.org/ticket/?id=1552, http://bugs.horde.org/ticket/?id=1553).
+* Added SQL scripts to create tables for SQL backends.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.4</release>
+    <api>0.0.4</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2003-09-09</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Default ftp driver to port 21.
+* Distinguish between connection and authentication errors in the ftp driver.
+* Clean up temporary files if the FTP driver wasn&apos;t able to retrieve a file.
+* Add exists() method.
+* Add emptyFolder() method.
+* Add $recursive parameter to all deleteFolder() implementations.
+* Add FTPS support.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Added a class for providing garbage collection; removed all Horde dependancies.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2002-10-22</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Added an ObjectVFS wrapper class, returns a ListItem on each call to listFolder, until no items are left.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2002-09-24</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/VFS/scripts/VFS/vfs.php b/framework/VFS/scripts/VFS/vfs.php
new file mode 100644 (file)
index 0000000..6ff719d
--- /dev/null
@@ -0,0 +1,389 @@
+#!@php_bin@
+<?php
+/**
+ * This is a command line interface for the VFS package.
+ *
+ * `vfs.php help' shows some usage instructions.
+ *
+ * $Horde: framework/VFS/scripts/VFS/vfs.php,v 1.2 2008/02/16 15:49:12 jan Exp $
+ *
+ * @package VFS
+ */
+
+/** PEAR */
+require_once 'PEAR.php';
+
+/** Console_Getopt */
+require_once 'Console/Getopt.php';
+
+/** DB */
+require_once 'DB.php';
+
+/** VFS */
+require_once 'VFS.php';
+
+/* Track errors. */
+ini_set('track_errors', true);
+
+/* Get command line options. */
+$argv = Console_Getopt::readPHPArgv();
+if (is_a($argv, 'PEAR_Error')) {
+    usage($argv->getMessage());
+}
+$cmd = array_shift($argv);
+$options = Console_Getopt::getopt2($argv, '', array());
+if (is_a($options, 'PEAR_Error')) {
+    usage($options->getMessage());
+}
+
+/* Show help? */
+if (!count($options[1]) || in_array('help', $options[1])) {
+    usage();
+}
+
+/* Get and execute the command. */
+$command = array_shift($options[1]);
+switch ($command) {
+
+case 'ls':
+    if (!count($options[1])) {
+        usage($command);
+    }
+    $params = Console_Getopt::getopt2($options[1], 'alR');
+    if (is_a($params, 'PEAR_Error')) {
+        usage($params->getMessage());
+    }
+    $path = array_shift($params[1]);
+    ls($path, mergeOptions($params[0]), $params[1]);
+    break;
+
+case 'cp':
+    if (!count($options[1])) {
+        usage($command);
+    }
+    $params = Console_Getopt::getopt2($options[1], 'arv');
+    if (is_a($params, 'PEAR_Error')) {
+        usage($params->getMessage());
+    }
+    $source = array_shift($params[1]);
+    $target = array_shift($params[1]);
+    cp($source, $target, mergeOptions($params[0]), $params[1]);
+    break;
+
+default:
+    usage();
+    break;
+
+}
+
+/**
+ * Lists the contents of the specified directory.
+ *
+ * @param string $url     The URL of the VFS backend
+ * @param array $argv     Additional options
+ * @param string $filter  Additional parameters
+ */
+function ls($url, $argv, $filter)
+{
+    $params = url2params($url);
+    $recursive = in_array('R', $argv);
+
+    $vfs = &vfs($params);
+    $list = $vfs->listFolder($params['path'],
+                             count($filter) ? $filter[0] : null,
+                             in_array('a', $argv));
+    if (is_a($list, 'PEAR_Error')) {
+        usage($list);
+    }
+    if (in_array('a', $argv)) {
+        $list = array_merge(array('.' => array('name' => '.'),
+                                  '..' => array('name' => '..')),
+                            $list);
+    }
+    $list = array_keys($list);
+    $max = max(array_map(create_function('$a', 'return strlen($a);'), $list)) + 2;
+
+    $line = '';
+    $dirs = array();
+    if ($recursive) {
+        echo $params['path'] . ":\n";
+    }
+    foreach ($list as $entry) {
+        if ($vfs->isFolder($params['path'], $entry)) {
+            $dirs[] = $entry;
+        }
+        $entry = sprintf('%-' . $max . 's', $entry);
+        if (strlen($line . $entry) > 80 && !empty($line)) {
+            echo $line . "\n";
+            $line = '';
+        }
+        $line .= $entry;
+    }
+    if (!empty($line)) {
+        echo $line . "\n";
+    }
+
+    if ($recursive && count($dirs)) {
+        foreach ($dirs as $dir) {
+            echo "\n";
+            ls($url . '/' . $dir, $argv, $filter);
+        }
+    }
+}
+
+/**
+ * Copies one or several files to a different location.
+ *
+ * @param string $source  The source file(s) or directory.
+ * @param string $target  The target file or directory.
+ * @param array $argv     Additional options
+ * @param string $filter  Additional parameters
+ */
+function cp($source, $target, $argv, $filter)
+{
+    $source_params = url2params($source);
+    $source_path = rtrim($source_params['path'], '/');
+    unset($source_params['path']);
+
+    $target_params = url2params($target);
+    $target_path = rtrim($target_params['path'], '/');
+    unset($target_params['path']);
+
+    if ($source_params == $target_params) {
+        // TODO: Shortcut with VFS::copy()
+    }
+
+    $source_vfs = &vfs($source_params);
+    $target_vfs = &vfs($target_params);
+
+    _cp($source_vfs, $target_vfs, $source_path, $target_path, $argv, $filter);
+}
+
+/**
+ * Copies one or several files to a different location.
+ *
+ * @param VFS $source_vfs      The source VFS object.
+ * @param VFS $target_vfs  The The target VFS object.
+ * @param string $source_path  The source file(s) or directory.
+ * @param string $target_path  The target file or directory.
+ * @param array $argv          Additional options
+ * @param string $filter       Additional parameters
+ */
+function _cp(&$source_vfs, &$target_vfs, $source_path, $target_path, $argv,
+             $filter)
+{
+    $source_object = basename($source_path);
+    $source_parent_path = dirname($source_path);
+
+    $target_object = basename($target_path);
+    $target_parent_path = dirname($target_path);
+
+    $recursive = in_array('r', $argv);
+
+    if ($source_vfs->isFolder($source_parent_path, $source_object)) {
+        if (!$recursive) {
+            echo "Skipping directory $source_path\n";
+            return;
+        }
+        if (!$target_vfs->isFolder($target_parent_path, $target_object)) {
+            if ($target_vfs->exists($target_parent_path, $target_object)) {
+                usage(PEAR::raiseError('You can\'t copy a folder on a file.'));
+            } else {
+                $target_vfs->createFolder($target_parent_path, $target_object);
+            }
+        }
+        if (!$target_vfs->isFolder($target_path, $source_object)) {
+            if ($target_vfs->exists($target_path, $source_object)) {
+                usage(PEAR::raiseError('You can\'t copy a folder on a file.'));
+            } elseif (!$target_vfs->exists($target_path, $source_object)) {
+                $target_vfs->createFolder($target_path, $source_object);
+            }
+        }
+
+        $list = $source_vfs->listFolder($source_path,
+                                        count($filter) ? $filter[0] : null,
+                                        in_array('a', $argv));
+        foreach ($list as $item) {
+            _cp($source_vfs, $target_vfs, $source_path . '/' . $item['name'],
+                $target_path . '/' . $source_object, $argv, $filter);
+        }
+        return;
+    }
+
+    $data = &$source_vfs->read($source_parent_path, $source_object);
+    if (is_a($data, 'PEAR_Error')) {
+        usage($data);
+    }
+
+    if ($target_vfs->isFolder($target_parent_path, $target_object)) {
+        if (in_array('v', $argv)) {
+            echo '`' . $source_path . '\' -> `' . $target_path . '/' .
+                $source_object . "'\n";
+        }
+        $result = $target_vfs->writeData($target_path, $source_object,
+                                         $data, true);
+        if (is_a($result, 'PEAR_Error')) {
+            usage($result);
+        }
+    } elseif ($target_vfs->isFolder(dirname($target_parent_path),
+                                    basename($target_parent_path))) {
+        if (in_array('v', $argv)) {
+            echo '`' . $source_path . '\' -> `' . $target_path . "'\n";
+        }
+        $result = $target_vfs->writeData($target_parent_path, $target_object,
+                                         $data, true);
+        if (is_a($result, 'PEAR_Error')) {
+            usage($result);
+        }
+    } else {
+        usage(PEAR::raiseError('"' . $target_parent_path .
+                               '" does not exist or is not a folder.'));
+    }
+}
+
+/**
+ * Shows some error and usage information.
+ *
+ * @param PEAR_Error $error  If specified its error messages will be displayed.
+ */
+function usage($error = null)
+{
+    if (is_a($error, 'PEAR_Error')) {
+        echo $error->getMessage() . "\n";
+        echo $error->getUserinfo() . "\n\n";
+    } else {
+        switch ($error) {
+        case 'ls':
+            echo 'Usage: vfs.php ls [-alR] <parameters>';
+            break;
+        case 'cp':
+            echo 'Usage: vfs.php cp [-arv] <parameters> <parameters>';
+            break;
+        }
+    }
+    $cmd = basename($GLOBALS['cmd']);
+
+    echo <<<USAGE
+Usage: $cmd [options] command [command-options] <parameters> ...
+
+Available commands:
+    ls - lists a folders content.
+    cp - copies a file or folder to a different location.
+
+<parameters> can be paths specified like an URL, e.g.:
+    file:///var/lib/horde/vfs/foo/bar
+    ftp://john:secret@ftp.example.com/foo/bar
+    ssh2://john:secret@ssh.example.com/foo/bar
+    sql://john:secret@localhost/mysql/horde/horde_vfs/foo/bar
+
+The SQL URL is build with the following scheme:
+    sql://[<user>[:<password>]@]<hostname>/<dbtype>/<database>/<table>[/<path>]
+
+USAGE;
+
+    exit;
+}
+
+/**
+ * Returns a VFS instance.
+ *
+ * @param array $params  A complete parameter set including the driver name
+ *                       for the requested VFS instance.
+ *
+ * @return VFS  An instance of the requested VFS backend.
+ */
+function &vfs($params)
+{
+    return VFS::singleton($params['driver'], $params);
+}
+
+/**
+ * Merges a set of options as returned by Console_Getopt::getopt2() into a
+ * single array.
+ *
+ * @param array $options  A two dimensional array with the options.
+ *
+ * @return array  A flat array with the options.
+ */
+function mergeOptions($options)
+{
+    $result = array();
+    foreach ($options as $param) {
+        $result = array_merge($result, $param);
+    }
+    return $result;
+}
+
+/**
+ * Parses a URL into a set of parameters that can be used to instantiate a
+ * VFS object.
+ *
+ * @todo Document the possible URL formats.
+ *
+ * @param string $url  A URL with all necessary information for a VFS driver.
+ *
+ * @return array  A hash with the parsed information.
+ */
+function url2params($url)
+{
+    $params = array('path' => '');
+    $url = @parse_url($url);
+    if (!is_array($url)) {
+        usage(PEAR::raiseError($php_errormsg));
+    }
+
+    $params['driver'] = $url['scheme'];
+    if (isset($url['host'])) {
+        $params['hostspec'] = $url['host'];
+    }
+    if (isset($url['port'])) {
+        $params['port'] = $url['port'];
+    }
+    if (isset($url['user'])) {
+        $params['username'] = $url['user'];
+    }
+    if (isset($url['pass'])) {
+        $params['password'] = $url['pass'];
+    }
+    if (isset($url['path'])) {
+        switch ($url['scheme']) {
+        case 'ftp':
+            $params['path'] = $url['path'];
+            break;
+        case 'file':
+            $params['vfsroot'] = $url['path'];
+            break;
+        case 'sql':
+            $path = explode('/', trim($url['path'], '/'));
+            if (count($path) == 2) {
+                usage(PEAR::raiseError('No table specified for SQL driver.'));
+            }
+            if (count($path) == 1) {
+                usage(PEAR::raiseError('No database and no table specified for SQL driver.'));
+            }
+            if (!count($path)) {
+                usage(PEAR::raiseError('No database type, database, and table specified for SQL driver.'));
+            }
+            $params['phptype'] = array_shift($path);
+            $params['database'] = array_shift($path);
+            $params['table'] = array_shift($path);
+            $params['path'] = implode('/', $path);
+            break;
+        case 'ssh2':
+            $params['path'] = $url['path'];
+            break;
+        default:
+            usage(PEAR::raiseError('Only the SQL, File, and FTP drivers are supported at the moment.'));
+            break;
+        }
+        if (isset($url['query'])) {
+            $queries = explode('&', $url['query']);
+            foreach ($queries as $query) {
+                $pair = explode('=', $query);
+                $params[$pair[0]] = isset($pair[1]) ? $pair[1] : true;
+            }
+        }
+    }
+
+    return $params;
+}
diff --git a/framework/VFS/test/VFS/AllTests.php b/framework/VFS/test/VFS/AllTests.php
new file mode 100644 (file)
index 0000000..19620a6
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * All tests for the VFS:: package.
+ *
+ * $Horde: framework/VFS/test/VFS/AllTests.php,v 1.2 2009/01/06 17:49:58 jan Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Horde
+ * @package    VFS
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=VFS
+ */
+
+/**
+ * Define the main method 
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'VFS_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * Combine the tests for this package.
+ *
+ * $Horde: framework/VFS/test/VFS/AllTests.php,v 1.2 2009/01/06 17:49:58 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category   Horde
+ * @package    VFS
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+class VFS_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - VFS');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('VFS_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'VFS_AllTests::main') {
+    VFS_AllTests::main();
+}
diff --git a/framework/VFS/test/VFS/KolabTest.php b/framework/VFS/test/VFS/KolabTest.php
new file mode 100644 (file)
index 0000000..fc7a161
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Test the Kolab based virtual file system.
+ *
+ * $Horde: framework/VFS/test/VFS/KolabTest.php,v 1.2 2009/01/06 17:49:58 jan Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Horde
+ * @package    VFS
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/Storage.php';
+
+require_once 'VFS.php';
+
+/**
+ * Test the Kolab based virtual file system.
+ *
+ * $Horde: framework/VFS/test/VFS/KolabTest.php,v 1.2 2009/01/06 17:49:58 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category   Horde
+ * @package    VFS
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+class VFS_KolabTest extends Horde_Kolab_Test_Storage
+{
+
+    /**
+     * Test setup.
+     *
+     * @return NULL
+     */
+    public function setUp()
+    {
+        $world = $this->prepareBasicSetup();
+
+        $this->assertTrue($world['auth']->authenticate('wrobel@example.org',
+                                                        array('password' => 'none')));
+
+        $this->_vfs = VFS::factory('kolab');
+    }
+
+    /**
+     * Test folder handling.
+     *
+     * @return NULL
+     */
+    public function testFolders()
+    {
+        $this->assertEquals(array(), $this->_vfs->listFolders());
+        $this->assertNoError($this->_vfs->createFolder('/', 'test'));
+        $this->assertEquals(1, count($this->_vfs->listFolders()));
+        $this->assertNoError($this->_vfs->autocreatePath('/a/b/c/d'));
+        $this->assertEquals(1, count($this->_vfs->listFolders('/')));
+        $this->assertEquals(3, count($this->_vfs->listFolders('/INBOX')));
+        $this->assertTrue($this->_vfs->exists('/INBOX/a', 'b'));
+        $a = $this->_vfs->listFolder('/INBOX/a/b', null, true, true);
+        $this->assertTrue(isset($a['c']));
+        $this->assertTrue($this->_vfs->isFolder('/INBOX/a/b', 'c'));
+        $this->assertTrue($this->_vfs->deleteFolder('/INBOX/a/b/c', 'd'));
+        $this->assertFalse($this->_vfs->exists('/INBOX/a/b/c', 'd'));
+        $this->assertTrue($this->_vfs->deleteFolder('/INBOX', 'a', true));
+    }
+
+    /**
+     * Test file handling.
+     *
+     * @return NULL
+     */
+    public function testFiles()
+    {
+    }
+}
diff --git a/framework/VFS/test/VFS/SmbTest.php b/framework/VFS/test/VFS/SmbTest.php
new file mode 100644 (file)
index 0000000..b6ac045
--- /dev/null
@@ -0,0 +1,367 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_VFS
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category   Horde
+ * @package    Horde_VFS
+ * @subpackage UnitTests
+ */
+class Horde_VFS_SmbTest extends PHPUnit_Framework_TestCase
+{
+    public function testParseListing()
+    {
+        $vfs = new VFS_smb();
+
+        $listing = $vfs->parseListing(file(dirname(__FILE__) . '/fixtures/samba1.txt'), null, true, false);
+        $this->assertType('array', $listing);
+        $this->assertEquals(7, count($listing));
+        var_export($listing);
+        $this->assertEquals(
+            array (
+                'SystemHiddenReadonlyArchive' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'SystemHiddenReadonlyArchive',
+                    'type' => '**dir',
+                    'date' => 1243426641,
+                    'size' => -1,
+                    ),
+                'Ein ziemlich langer Ordner mit vielen Buchstaben, der nicht kurz ist' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Ein ziemlich langer Ordner mit vielen Buchstaben, der nicht kurz ist',
+                    'type' => '**dir',
+                    'date' => 1243426451,
+                    'size' => -1,
+                    ),
+                'Eine ziemlich lange Datei mit vielen Buchstaben, die nicht kurz ist.txt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Eine ziemlich lange Datei mit vielen Buchstaben, die nicht kurz ist.txt',
+                    'type' => 'txt',
+                    'date' => 1243426482,
+                    'size' => '0',
+                    ),
+                'Ordner mit Sonderzeichen & ( ) _ - toll' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Ordner mit Sonderzeichen & ( ) _ - toll',
+                    'type' => '**dir',
+                    'date' => 1243426505,
+                    'size' => -1,
+                    ),
+                'Datei mit SOnderzeichen Â¿ ÃƒÂ§ Âµ Â° juhuuu.txt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Datei mit SOnderzeichen Â¿ ÃƒÂ§ Âµ Â° juhuuu.txt',
+                    'type' => 'txt',
+                    'date' => 1243426538,
+                    'size' => '0',
+                    ),
+                'SystemHiddenReadonlyArchive.txt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'SystemHiddenReadonlyArchive.txt',
+                    'type' => 'txt',
+                    'date' => 1243426592,
+                    'size' => '0',
+                    ),
+                'SystemHiddenReadonlyArchive.txte' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'SystemHiddenReadonlyArchive.txte',
+                    'type' => 'txte',
+                    'date' => 1243430322,
+                    'size' => '31',
+                    ),
+                ),
+            $listing);
+
+        $listing = $vfs->parseListing(file(dirname(__FILE__) . '/fixtures/samba2.txt'), null, true, false);
+        $this->assertType('array', $listing);
+        $this->assertEquals(26, count($listing));
+        $this->assertEquals(
+            array (
+                'tmp' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'tmp',
+                    'type' => '**dir',
+                    'date' => 1199697783,
+                    'size' => -1,
+                    ),
+                'Der Fischer und seine Frau Märchen.odt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Der Fischer und seine Frau Märchen.odt',
+                    'type' => 'odt',
+                    'date' => 1169758536,
+                    'size' => '22935',
+                    ),
+                'Tänze' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Tänze',
+                    'type' => '**dir',
+                    'date' => 1169756813,
+                    'size' => -1,
+                    ),
+                'Availabilities+rates EE-Dateien' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates EE-Dateien',
+                    'type' => '**dir',
+                    'date' => 1126615613,
+                    'size' => -1,
+                    ),
+                'Briefkopf.odt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Briefkopf.odt',
+                    'type' => 'odt',
+                    'date' => 1137753731,
+                    'size' => '9564',
+                    ),
+                'Deckblatt.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Deckblatt.pdf',
+                    'type' => 'pdf',
+                    'date' => 1196284002,
+                    'size' => '18027',
+                    ),
+                'Babymassage.sxw' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Babymassage.sxw',
+                    'type' => 'sxw',
+                    'date' => 1102376414,
+                    'size' => '9228',
+                    ),
+                'Gutschein.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Gutschein.pdf',
+                    'type' => 'pdf',
+                    'date' => 1168102242,
+                    'size' => '10621',
+                    ),
+                'Die zertanzten Schuh.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Die zertanzten Schuh.pdf',
+                    'type' => 'pdf',
+                    'date' => 1169483565,
+                    'size' => '257955',
+                    ),
+                'Flyer Im Takt.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Flyer Im Takt.pdf',
+                    'type' => 'pdf',
+                    'date' => 1169891684,
+                    'size' => '42905',
+                    ),
+                'Availabilities+rates EE.doc' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates EE.doc',
+                    'type' => 'doc',
+                    'date' => 1124044046,
+                    'size' => '1407488',
+                    ),
+                'Availabilities+rates EE.htm' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates EE.htm',
+                    'type' => 'htm',
+                    'date' => 1126615336,
+                    'size' => '262588',
+                    ),
+                'tt0208m_.ttf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'tt0208m_.ttf',
+                    'type' => 'ttf',
+                    'date' => 1111250096,
+                    'size' => '47004',
+                    ),
+                'Alte Dateien.zip' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Alte Dateien.zip',
+                    'type' => 'zip',
+                    'date' => 1179697912,
+                    'size' => '5566512',
+                    ),
+                'Availabilities+rates SQ-Dateien' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates SQ-Dateien',
+                    'type' => '**dir',
+                    'date' => 1126615567,
+                    'size' => -1,
+                    ),
+                'Bobath-Befund.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Bobath-Befund.pdf',
+                    'type' => 'pdf',
+                    'date' => 1196282600,
+                    'size' => '123696',
+                    ),
+                'Availabilities+rates SQ.doc' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates SQ.doc',
+                    'type' => 'doc',
+                    'date' => 1124044062,
+                    'size' => '109056',
+                    ),
+                'Availabilities+rates SQ.htm' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Availabilities+rates SQ.htm',
+                    'type' => 'htm',
+                    'date' => 1126615290,
+                    'size' => '266079',
+                    ),
+                'tt0586m_.ttf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'tt0586m_.ttf',
+                    'type' => 'ttf',
+                    'date' => 1111250098,
+                    'size' => '35928',
+                    ),
+                'Gartenkonzept SZOE.html' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Gartenkonzept SZOE.html',
+                    'type' => 'html',
+                    'date' => 1199698030,
+                    'size' => '168801',
+                    ),
+                '.DS_Store' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => '.DS_Store',
+                    'type' => 'ds_store',
+                    'date' => 1110391107,
+                    'size' => '12292',
+                    ),
+                'Pfefferkuchenmann.odt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Pfefferkuchenmann.odt',
+                    'type' => 'odt',
+                    'date' => 1166644679,
+                    'size' => '14399',
+                    ),
+                'Sockenstrickanleitung mit Bildern.sxw' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Sockenstrickanleitung mit Bildern.sxw',
+                    'type' => 'sxw',
+                    'date' => 1104172329,
+                    'size' => '9518',
+                    ),
+                'Gartenkonzept SZOE.doc' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Gartenkonzept SZOE.doc',
+                    'type' => 'doc',
+                    'date' => 1180365752,
+                    'size' => '32959488',
+                    ),
+                'Gartenkonzept SZOE.odt' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Gartenkonzept SZOE.odt',
+                    'type' => 'odt',
+                    'date' => 1180365528,
+                    'size' => '32526103',
+                    ),
+                'Gartenkonzept SZOE.pdf' =>
+                array (
+                    'owner' => '',
+                    'group' => '',
+                    'perms' => '',
+                    'name' => 'Gartenkonzept SZOE.pdf',
+                    'type' => 'pdf',
+                    'date' => 1179697180,
+                    'size' => '32632182',
+                    ),
+                ),
+            $listing);
+    }
+
+}
diff --git a/framework/VFS/test/VFS/fixtures/samba1.txt b/framework/VFS/test/VFS/fixtures/samba1.txt
new file mode 100755 (executable)
index 0000000..f5388e0
--- /dev/null
@@ -0,0 +1,11 @@
+  .                                   D        0  Wed May 27 15:18:42 2009
+  ..                                  D        0  Thu May 28 16:04:56 2009
+  SystemHiddenReadonlyArchive     DAHSR        0  Wed May 27 14:17:21 2009
+  Ein ziemlich langer Ordner mit vielen Buchstaben, der nicht kurz ist      D        0  Wed May 27 14:14:11 2009
+  Eine ziemlich lange Datei mit vielen Buchstaben, die nicht kurz ist.txt      A        0  Wed May 27 14:14:42 2009
+  Ordner mit Sonderzeichen & ( ) _ - toll      D        0  Wed May 27 14:15:05 2009
+  Datei mit SOnderzeichen Â¿ â‚¬ Â§ Âµ Â° juhuuu.txt      A        0  Wed May 27 14:15:38 2009
+  SystemHiddenReadonlyArchive.txt   AHSR        0  Wed May 27 14:16:32 2009
+  SystemHiddenReadonlyArchive.txte    AHR       31  Wed May 27 15:18:42 2009
+
+               49152 blocks of size 65536. 2 blocks available
diff --git a/framework/VFS/test/VFS/fixtures/samba2.txt b/framework/VFS/test/VFS/fixtures/samba2.txt
new file mode 100644 (file)
index 0000000..7f8a078
--- /dev/null
@@ -0,0 +1,30 @@
+  .                                   D        0  Fri Apr 24 13:29:13 2009
+  ..                                  D        0  Fri Jun  6 22:23:04 2008
+  tmp                                 D        0  Mon Jan  7 10:23:03 2008
+  Der Fischer und seine Frau Märchen.odt      A    22935  Thu Jan 25 21:55:36 2007
+  Tänze                              D        0  Thu Jan 25 21:26:53 2007
+  Availabilities+rates EE-Dateien      D        0  Tue Sep 13 14:46:53 2005
+  Briefkopf.odt                       A     9564  Fri Jan 20 11:42:11 2006
+  Deckblatt.pdf                       A    18027  Wed Nov 28 22:06:42 2007
+  Babymassage.sxw                     A     9228  Tue Dec  7 00:40:14 2004
+  Gutschein.pdf                       A    10621  Sat Jan  6 17:50:42 2007
+  Die zertanzten Schuh.pdf            A   257955  Mon Jan 22 17:32:45 2007
+  Flyer Im Takt.pdf                   A    42905  Sat Jan 27 10:54:44 2007
+  Availabilities+rates EE.doc         A  1407488  Sun Aug 14 20:27:26 2005
+  Availabilities+rates EE.htm         A   262588  Tue Sep 13 14:42:16 2005
+  tt0208m_.ttf                        A    47004  Sat Mar 19 17:34:56 2005
+  Alte Dateien.zip                    A  5566512  Sun May 20 23:51:52 2007
+  Availabilities+rates SQ-Dateien      D        0  Tue Sep 13 14:46:07 2005
+  Bobath-Befund.pdf                   A   123696  Wed Nov 28 21:43:20 2007
+  Availabilities+rates SQ.doc         A   109056  Sun Aug 14 20:27:42 2005
+  Availabilities+rates SQ.htm         A   266079  Tue Sep 13 14:41:30 2005
+  tt0586m_.ttf                        A    35928  Sat Mar 19 17:34:58 2005
+  Gartenkonzept SZOE.html                 168801  Mon Jan  7 10:27:10 2008
+  .DS_Store                          AH    12292  Wed Mar  9 18:58:27 2005
+  Pfefferkuchenmann.odt                    14399  Wed Dec 20 20:57:59 2006
+  Sockenstrickanleitung mit Bildern.sxw            9518  Mon Dec 27 19:32:09 2004
+  Gartenkonzept SZOE.doc                32959488  Mon May 28 17:22:32 2007
+  Gartenkonzept SZOE.odt              A 32526103  Mon May 28 17:18:48 2007
+  Gartenkonzept SZOE.pdf              A 32632182  Sun May 20 23:39:40 2007
+
+               48828 blocks of size 2097152. 14773 blocks available
diff --git a/framework/VFS_ISOWriter/ISOWriter.php b/framework/VFS_ISOWriter/ISOWriter.php
new file mode 100644 (file)
index 0000000..999302b
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+
+require_once 'PEAR.php';
+
+/**
+ * VFS API for abstracted creation of ISO (CD-ROM) filesystems.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter.php,v 1.13 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter {
+
+    /**
+     * A VFS object used for reading the source files
+     *
+     * @var VFS
+     */
+    var $_sourceVfs = null;
+
+    /**
+     * A VFS object used for writing the ISO image
+     *
+     * @var VFS
+     */
+    var $_targetVfs = null;
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * Constructs a new VFS_ISOWriter object
+     *
+     * @param array $params  A hash containing parameters.
+     */
+    function VFS_ISOWriter(&$sourceVfs, &$targetVfs, $params)
+    {
+        $this->_sourceVfs = &$sourceVfs;
+        $this->_targetVfs = &$targetVfs;
+        $this->_params = $params;
+    }
+
+    /**
+     * Create the ISO image
+     *
+     * @abstract
+     *
+     * @return mixed  Null or PEAR_Error on failure.
+     */
+    function process()
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Attempt to create a concrete VFS_ISOWriter subclass.
+     *
+     * This method uses its parameters and checks the system to determine
+     * the most appropriate subclass to use for building ISO images.  If
+     * none is found, an error is raised.
+     *
+     * @param object &$sourceVfs      Reference to the VFS object from which
+     *                                the files will be read to create the
+     *                                ISO image.
+     * @param object &$targetVfs      Reference to the VFS object to which the
+     *                                ISO image will be written.
+     * @param array $params           Hash of parameters for creating the
+     *                                image:
+     *              'sourceRoot' =>     A directory in the source VFS for
+     *                                  files to be read from for the image.
+     *              'targetFile' =>     Path and filename of the ISO file to
+     *                                  write into the target VFS.
+     *
+     * @return object                 A newly created concrete VFS_ISOWriter
+     *                                subclass, or a PEAR_Error on an error.
+     */
+    function &factory(&$sourceVfs, &$targetVfs, $params)
+    {
+        if (empty($params['targetFile'])) {
+            return PEAR::raiseError(_("Cannot proceed without 'targetFile' parameter."));
+        }
+        if (empty($params['sourceRoot'])) {
+            $params['sourceRoot'] = '/';
+        }
+
+        /* Right now, mkisofs is the only driver, but make sure we can
+         * support it. */
+        require_once dirname(__FILE__) . '/ISOWriter/mkisofs.php';
+        if (VFS_ISOWriter_mkisofs::strategyAvailable()) {
+            $isowriter = new VFS_ISOWriter_mkisofs($sourceVfs, $targetVfs,
+                                                    $params);
+            return $isowriter;
+        }
+
+        return PEAR::raiseError(_("No available strategy for making ISO images."));
+    }
+
+}
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy.php b/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy.php
new file mode 100644 (file)
index 0000000..d76f0d1
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Encapsulate strategies for getting a real, local filesystem structure from
+ * a VFS.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealInputStrategy.php,v 1.14 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealInputStrategy {
+
+    /**
+     * A reference to the source VFS we want to read.
+     *
+     * @var VFS
+     */
+    var $_sourceVfs = null;
+
+    /**
+     * The root directory within the source VFS
+     *
+     * @var string
+     */
+    var $_sourceRoot;
+
+    function VFS_ISOWriter_RealInputStrategy(&$sourceVfs, $sourceRoot)
+    {
+        $this->_sourceVfs = &$sourceVfs;
+        $this->_sourceRoot = &$sourceRoot;
+    }
+
+    /**
+     * Get a real path to the input tree.
+     *
+     * @abstract
+     * @return mixed    A string with the real path, or PEAR_Error on failure.
+     */
+    function getRealPath()
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Indicate we are finished with this input strategy.
+     *
+     * @abstract
+     * @return mixed        Null or PEAR_Error on failure.
+     */
+    function finished()
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Decide which strategy to use to get a real FS and create it.
+     *
+     * @static
+     *
+     * @param object &$sourceVfs        The VFS we want to read from.
+     * @param string $sourceRoot        The root directory in that VFS.
+     * @return object   A concrete strategy or PEAR_Error if no strategy is
+     *                  available.
+     */
+    function &factory(&$sourceVfs, $sourceRoot)
+    {
+        if (strtolower(get_class($sourceVfs)) == 'vfs_file') {
+            $method = 'direct';
+        } else {
+            $method = 'copy';
+        }
+
+        include_once dirname(__FILE__) . '/RealInputStrategy/' . $method . '.php';
+        $class = 'VFS_ISOWriter_RealInputStrategy_' . $method;
+        if (class_exists($class)) {
+            $strategy = new $class($sourceVfs, $sourceRoot);
+        } else {
+            $strategy = PEAR::raiseError(sprintf(_("Could not load strategy \"%s\"."),
+                                                 $method));
+        }
+
+        return $strategy;
+    }
+
+}
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/copy.php b/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/copy.php
new file mode 100644 (file)
index 0000000..426a09d
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * Strategy for copying input tree out of a VFS
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/copy.php,v 1.10 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealInputStrategy_copy extends VFS_ISOWriter_RealInputStrategy {
+
+    var $_tempPath = null;
+
+    function getRealPath()
+    {
+        if (is_null($this->_tempPath)) {
+            $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp',
+                                   'c:\windows\temp', 'c:\winnt\temp');
+
+            /* First, try PHP's upload_tmp_dir directive. */
+            $tmp = ini_get('upload_tmp_dir');
+
+            /* Otherwise, try to determine the TMPDIR environment
+             * variable. */
+            if (empty($tmp)) {
+                $tmp = getenv('TMPDIR');
+            }
+
+            /* If we still cannot determine a value, then cycle through a
+             * list of preset possibilities. */
+            while (empty($tmp) && count($tmp_locations)) {
+                $tmp_check = array_shift($tmp_locations);
+                if (@is_dir($tmp_check)) {
+                    $tmp = $tmp_check;
+                }
+            }
+
+            if (empty($tmp)) {
+                return PEAR::raiseError(_("Cannot find a temporary directory."));
+            }
+
+            $this->_tempPath = tempnam($tmp, 'isod');
+            @unlink($this->_tempPath);
+
+            $res = $this->_copyToTempPath();
+            if (is_a($res, 'PEAR_Error')) {
+                return $res;
+            }
+        }
+
+        return $this->_tempPath;
+    }
+
+    function finished()
+    {
+        return VFS_ISOWriter_RealInputStrategy_copy::_removeRecursive($this->_tempPath);
+    }
+
+    function _removeRecursive($path)
+    {
+        $dh = @opendir($path);
+        if (!is_resource($dh)) {
+            return PEAR::raiseError(sprintf(_("Could not open directory \"%s\"."),
+                                            $path));
+        }
+        while (($ent = readdir($dh)) !== false) {
+            if ($ent == '.' || $ent == '..') {
+                continue;
+            }
+
+            $full = sprintf('%s/%s', $path, $ent);
+            if (is_dir($full)) {
+                $res = VFS_ISOWriter_RealInputStrategy_copy::_removeRecursive($full);
+                if (is_a($res, 'PEAR_Error')) {
+                    return $res;
+                }
+            } else {
+                if (!@unlink($full)) {
+                    return PEAR::raiseError(sprintf(_("Could not unlink \"%s\"."),
+                                                    $full));
+                }
+            }
+        }
+        closedir($dh);
+
+        if (!@rmdir($path)) {
+            return PEAR::raiseError(sprintf(_("Could not rmdir \"%s\"."), $full));
+        }
+    }
+
+    function _copyToTempPath()
+    {
+        $dirStack = array('');
+
+        while (count($dirStack) > 0) {
+            $dir = array_shift($dirStack);
+            if (empty($dir)) {
+                $target = $this->_tempPath;
+            } else {
+                $target = sprintf('%s/%s', $this->_tempPath, $dir);
+            }
+            if (!@mkdir($target)) {
+                return PEAR::raiseError(sprintf(_("Could not mkdir \"%s\"."), $target));
+            }
+
+            $sourcePath = $this->_sourceRoot;
+            if (!empty($dir)) {
+                $sourcePath .= '/' . $dir;
+            }
+
+            $list = $this->_sourceVfs->listFolder($sourcePath, null, true);
+            if (is_a($list, 'PEAR_Error')) {
+                return $list;
+            }
+
+            foreach ($list as $entry) {
+                if ($entry['type'] == '**dir') {
+                    if (empty($dir)) {
+                        $dirStack[] = $entry['name'];
+                    } else {
+                        $dirStack[] = sprintf('%s/%s', $dir, $entry['name']);
+                    }
+                } else {
+                    $data = $this->_sourceVfs->read($sourcePath, $entry['name']);
+                    if (is_a($data, 'PEAR_Error')) {
+                        return $data;
+                    }
+
+                    $targetFile = sprintf('%s/%s', $target, $entry['name']);
+                    $fh = @fopen($targetFile, 'w');
+                    if (!is_resource($fh)) {
+                        return PEAR::raiseError(sprintf(_("Could not open \"%s\" for writing."), $targetFile));
+                    }
+                    if (fwrite($fh, $data) != strlen($data)) {
+                        return PEAR::raiseError(sprintf(_("Error writing \"%s\"."), $targetFile));
+                    }
+                    fclose($fh);
+                }
+            }
+        }
+    }
+
+}
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/direct.php b/framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/direct.php
new file mode 100644 (file)
index 0000000..fd6874e
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * Strategy for directly accessing input tree in a 'file' VFS
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealInputStrategy/direct.php,v 1.9 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealInputStrategy_direct extends VFS_ISOWriter_RealInputStrategy {
+
+    function getRealPath()
+    {
+        return $this->_sourceVfs->_getNativePath($this->_sourceRoot);
+    }
+
+    function finished()
+    {
+    }
+
+}
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy.php b/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy.php
new file mode 100644 (file)
index 0000000..152adcd
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Encapsulate strategies for ability to write output to real file.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy.php,v 1.15 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealOutputStrategy {
+
+    /**
+     * The VFS to which we will write the file.
+     *
+     * @var VFS
+     */
+    var $_targetVfs;
+
+    /**
+     * Where to store the file in the VFS.
+     *
+     * @var string
+     */
+    var $_targetFile;
+
+    /**
+     * Constructor
+     *
+     * @param object &$targetVfs        The VFS to which we will write the
+     *                                  file.
+     * @param string $targetFile        The path and name of file to write.
+     */
+    function VFS_ISOWriter_RealOutputStrategy(&$targetVfs, $targetFile)
+    {
+        $this->_targetVfs = &$targetVfs;
+        $this->_targetFile = $targetFile;
+    }
+
+    /**
+     * Select and create a concrete strategy for using a real output file.
+     *
+     * @param object &$targetVfs        The VFS to which we will write the
+     *                                  result.
+     * @param string $targetFile        The path and filename of the target
+     *                                  file within the VFS.
+     * @return object   A concrete output strategy object or PEAR_Error on
+     *                  failure.
+     */
+    function &factory(&$targetVfs, $targetFile)
+    {
+        if (strtolower(get_class($targetVfs)) == 'vfs_file') {
+            $method = 'direct';
+        } else {
+            $method = 'copy';
+        }
+
+        include_once dirname(__FILE__) . '/RealOutputStrategy/' . $method . '.php';
+        $class = 'VFS_ISOWriter_RealOutputStrategy_' . $method;
+        if (class_exists($class)) {
+            $strategy = new $class($targetVfs, $targetFile);
+        } else {
+            $strategy = PEAR::raiseError(sprintf(_("Could not load strategy \"%s\"."),
+                                                 $method));
+        }
+
+        return $strategy;
+    }
+
+    /**
+     * Get a real filesystem filename we can write to.
+     *
+     * @abstract
+     * @return string   The filename or PEAR_Error on failure.
+     */
+    function getRealFilename()
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+    /**
+     * Indicate that we're done writing to the real file.
+     *
+     * @abstract
+     * @return mixed    Null or PEAR_Error on failure.
+     */
+    function finished()
+    {
+        return PEAR::raiseError(_("Not implemented."));
+    }
+
+}
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/copy.php b/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/copy.php
new file mode 100644 (file)
index 0000000..6c6f713
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Strategy for writing file to temporary directory, then copying to VFS.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/copy.php,v 1.10 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealOutputStrategy_copy extends VFS_ISOWriter_RealOutputStrategy {
+
+    var $_tempFilename = null;
+
+    /**
+     * Get a real filename to which we can write.
+     *
+     * In this implementation, we create and store a temporary filename.
+     */
+    function getRealFilename()
+    {
+        if (is_null($this->_tempFilename)) {
+
+            $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp',
+                                   'c:\windows\temp', 'c:\winnt\temp');
+
+            /* First, try PHP's upload_tmp_dir directive. */
+            $tmp = ini_get('upload_tmp_dir');
+
+            /* Otherwise, try to determine the TMPDIR environment
+             * variable. */
+            if (empty($tmp)) {
+                $tmp = getenv('TMPDIR');
+            }
+
+            /* If we still cannot determine a value, then cycle through a
+             * list of preset possibilities. */
+            while (empty($tmp) && count($tmp_locations)) {
+                $tmp_check = array_shift($tmp_locations);
+                if (@is_dir($tmp_check)) {
+                    $tmp = $tmp_check;
+                }
+            }
+
+            if (empty($tmp)) {
+                return PEAR::raiseError(_("Cannot find a temporary directory."));
+            }
+
+            $this->_tempFilename = tempnam($tmp, 'iso');
+        }
+
+        return $this->_tempFilename;
+    }
+
+    function finished()
+    {
+        if (empty($this->_tempFilename)) {
+            return;
+        }
+        if (!file_exists($this->_tempFilename)) {
+            return;
+        }
+
+        if (preg_match('!^(.*)/([^/]*)$!', $this->_targetFile, $matches)) {
+            $dir = $matches[1];
+            $file = $matches[2];
+        } else {
+            $dir = '';
+            $file = $this->_targetFile;
+        }
+
+        $res = $this->_targetVfs->write($dir, $file, $this->_tempFilename,
+                                        true);
+        @unlink($this->_tempFilename);
+        $this->_tempFilename = null;
+        return $res;
+    }
+
+}
+
diff --git a/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/direct.php b/framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/direct.php
new file mode 100644 (file)
index 0000000..9cafc32
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Strategy for directly writing output file to VFS.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/RealOutputStrategy/direct.php,v 1.10 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_RealOutputStrategy_direct extends VFS_ISOWriter_RealOutputStrategy {
+
+    function getRealFilename()
+    {
+        /* So we shouldn't be accessing _getNativePath().  If we had real
+         * access control, that would be protected and we'd be a friend, as
+         * that is the point of this excercise. */
+        $filename = $this->_targetVfs->_getNativePath($this->_targetFile);
+
+        /* Make sure the path to the file exists. */
+        $dir = dirname($filename);
+        while (!@is_dir($dir)) {
+            if (!@mkdir($dir, 0755)) {
+                return PEAR::raiseError(sprintf(_("Could not mkdir \"%s\"."),
+                                                $dir));
+            }
+            $dir = dirname($dir);
+        }
+
+        return $filename;
+    }
+
+    function finished()
+    {
+        /* Nothing to do. */
+    }
+
+}
+
diff --git a/framework/VFS_ISOWriter/ISOWriter/mkisofs.php b/framework/VFS_ISOWriter/ISOWriter/mkisofs.php
new file mode 100644 (file)
index 0000000..7a703f0
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * Driver for using mkisofs for creating ISO images.
+ *
+ * $Horde: framework/VFS_ISOWriter/ISOWriter/mkisofs.php,v 1.10 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jason M. Felice <jason.m.felice@gmail.com>
+ * @package VFS_ISO
+ * @since   Horde 3.0
+ */
+class VFS_ISOWriter_mkisofs extends VFS_ISOWriter {
+
+    function process()
+    {
+        require_once dirname(__FILE__) . '/RealInputStrategy.php';
+        $inputStrategy = &VFS_ISOWriter_RealInputStrategy::factory($this->_sourceVfs, $this->_params['sourceRoot']);
+        if (is_a($inputStrategy, 'PEAR_Error')) {
+            return $inputStrategy;
+        }
+
+        require_once dirname(__FILE__) . '/RealOutputStrategy.php';
+        $outputStrategy = &VFS_ISOWriter_RealOutputStrategy::factory($this->_targetVfs, $this->_params['targetFile']);
+        if (is_a($outputStrategy, 'PEAR_Error')) {
+            return $outputStrategy;
+        }
+
+        $cmd = sprintf('mkisofs -quiet -r -J -o %s %s >/dev/null',
+                       escapeshellarg($outputStrategy->getRealFilename()),
+                       escapeshellarg($inputStrategy->getRealPath()));
+        $res = system($cmd, $ec);
+
+        /* Could be a lot of space used.  Give both a chance to clean up even
+         * if one errors out. */
+        $finRes1 = $inputStrategy->finished();
+        $finRes2 = $outputStrategy->finished();
+        if (is_a($finRes1, 'PEAR_Error')) {
+            return $finRes1;
+        }
+        if (is_a($finRes2, 'PEAR_Error')) {
+            return $finRes2;
+        }
+
+        if ($res === false) {
+            return PEAR::raiseError(_("Unable to run 'mkisofs'."));
+        }
+        if ($ec != 0) {
+            return PEAR::raiseError(sprintf(_("mkisofs error code %d while making ISO."), $ec));
+        }
+    }
+
+    /**
+     * Determine if we can use this driver to make images
+     *
+     * @static
+     *
+     * @return boolean  Whether we can use this strategy for making ISO images.
+     */
+    function strategyAvailable()
+    {
+        /* Check if we can find and execute the `mkisofs' command. */
+        $res = system("mkisofs -help >/dev/null 2>&1", $ec);
+        if ($res === false) {
+            return false;
+        }
+        if ($ec != 0) {
+            return false;
+        }
+        return true;
+    }
+
+}
+
diff --git a/framework/VFS_ISOWriter/package.xml b/framework/VFS_ISOWriter/package.xml
new file mode 100644 (file)
index 0000000..488af08
--- /dev/null
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>VFS_ISOWriter</name>
+ <channel>pear.horde.org</channel>
+ <summary>Virtual File System ISO-writing API</summary>
+ <description>This package provides VFS extensions for writing ISO CD images.  It currently has drivers for:
+* mkisofs
+  
+ </description>
+ <lead>
+  <name>Jason M. Felice</name>
+  <user>eraserhd</user>
+  <email>jason.m.felice@gmail.com</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-09</date>
+ <time>00:06:09</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="ISOWriter">
+    <dir name="RealInputStrategy">
+     <file baseinstalldir="/VFS" name="copy.php" role="php" />
+     <file baseinstalldir="/VFS" name="direct.php" role="php" />
+    </dir> <!-- /ISOWriter/RealInputStrategy -->
+    <dir name="RealOutputStrategy">
+     <file baseinstalldir="/VFS" name="copy.php" role="php" />
+     <file baseinstalldir="/VFS" name="direct.php" role="php" />
+    </dir> <!-- /ISOWriter/RealOutputStrategy -->
+    <file baseinstalldir="/VFS" name="mkisofs.php" role="php" />
+    <file baseinstalldir="/VFS" name="RealInputStrategy.php" role="php" />
+    <file baseinstalldir="/VFS" name="RealOutputStrategy.php" role="php" />
+   </dir> <!-- /ISOWriter -->
+   <dir name="tests">
+    <file baseinstalldir="/VFS" name="inputstrategy.phpt" role="test" />
+    <file baseinstalldir="/VFS" name="isowriter.phpt" role="test" />
+    <file baseinstalldir="/VFS" name="outputstrategy.phpt" role="test" />
+   </dir> <!-- /tests -->
+   <file baseinstalldir="/VFS" name="ISOWriter.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>VFS</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-10-06</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/VFS_ISOWriter/tests/inputstrategy.phpt b/framework/VFS_ISOWriter/tests/inputstrategy.phpt
new file mode 100644 (file)
index 0000000..81aa313
--- /dev/null
@@ -0,0 +1,124 @@
+--TEST--
+VFS_ISOWriter_RealInputStrategy:: and drivers
+--FILE--
+<?php
+
+require_once 'VFS.php';
+require_once 'VFS/file.php';
+require_once dirname(__FILE__) . '/../ISOWriter/RealInputStrategy.php';
+
+/**
+ * This class is to make a file driver for VFS which isn't treated as a file
+ * driver (strategies detect based on class name).
+ */
+class VFS_notfile extends VFS_file {
+}
+
+echo "Load... ok\n";
+
+testDirectInputStrategy();
+testCopyInputStrategy();
+
+function testDirectInputStrategy()
+{
+    echo "Testing direct input strategy... ";
+
+    $vfs = &VFS::factory('file', array('vfsroot' => '/tmp'));
+    testInputStrategy($vfs, 'vfs_isowriter_realinputstrategy_direct');
+}
+
+function testCopyInputStrategy()
+{
+    echo "Testing copy input strategy... ";
+
+    $vfs = &new VFS_notfile(array('vfsroot' => '/tmp'));
+    testInputStrategy($vfs, 'vfs_isowriter_realinputstrategy_copy');
+}
+
+function testInputStrategy(&$vfs, $expectClass)
+{
+    /* Contents for generated files. */
+    $contents = array('a' => md5(uniqid('a', true)),
+                      'd/b' => md5(uniqid('b', true)),
+                      'd/e/c' => md5(uniqid('c', true)));
+
+    foreach ($contents as $name => $data) {
+        if (preg_match('!^(.*)/([^/]*)$!', $name, $matches)) {
+            $dir = $matches[1];
+            $file = $matches[2];
+        } else {
+            $dir = '';
+            $file = $name;
+        }
+
+        $res = $vfs->writeData('root/' . $dir, $file, $data, true);
+        if (is_a($res, 'PEAR_Error')) {
+            printf("ERROR(1): %s: %s\n", $name, $res->getMessage());
+            return;
+        }
+    }
+
+    $inputStrategy = &VFS_ISOWriter_RealInputStrategy::factory($vfs, 'root');
+    if (is_a($inputStrategy, 'PEAR_Error')) {
+        printf("ERROR(2): %s\n", $inputStrategy->getMessage());
+        return;
+    }
+
+    if ($expectClass != get_class($inputStrategy)) {
+        printf("ERROR(3): expected class '%s', got '%s'.\n", $expectClass,
+               get_class($inputStrategy));
+        return;
+    }
+
+    $realPath = $inputStrategy->getRealPath();
+    if (is_a($realPath, 'PEAR_Error')) {
+        printf("ERROR(4): %s\n", $realPath->getMessage());
+        return;
+    }
+
+    foreach ($contents as $name => $data) {
+        $path = sprintf('%s/%s', $realPath, $name);
+        if (!file_exists($path)) {
+            printf("ERROR(5): file '%s' does not exist.\n", $path);
+            return;
+        }
+
+        $fh = @fopen($path, 'r');
+        if (!is_resource($fh)) {
+            printf("ERROR(6): could not open '%s' for reading.\n", $path);
+            return;
+        }
+        $fileData = fread($fh, filesize($path));
+        fclose($fh);
+        if ($fileData != $data) {
+            printf("ERROR(7): %s: contents should be '%s' but got '%s'.\n",
+                   $path, $data, $fileData);
+            return;
+        }
+    }
+
+    $res = $inputStrategy->finished();
+    if (is_a($res, 'PEAR_Error')) {
+        printf("ERROR(8): %s\n", $res->getMessage());
+        return;
+    }
+
+    foreach ($contents as $name => $data) {
+        if (preg_match('!^(.*)/([^/]*)$!', $name, $matches)) {
+            $dir = $matches[1];
+            $file = $matches[2];
+        } else {
+            $dir = '';
+            $file = $name;
+        }
+        $vfs->deleteFile('root/' . $dir, $file);
+    }
+
+    $vfs->deleteFolder('root', 'd', true);
+    echo "ok\n";
+}
+
+--EXPECT--
+Load... ok
+Testing direct input strategy... ok
+Testing copy input strategy... ok
diff --git a/framework/VFS_ISOWriter/tests/isowriter.phpt b/framework/VFS_ISOWriter/tests/isowriter.phpt
new file mode 100644 (file)
index 0000000..26b9e2a
--- /dev/null
@@ -0,0 +1,104 @@
+--TEST--
+VFS_ISOWriter:: and drivers
+--FILE--
+<?php
+
+require_once 'VFS.php';
+require_once dirname(__FILE__) . '/../ISOWriter.php';
+
+echo "Load... ok\n";
+
+echo "Creating VFS... ";
+$vfs = &VFS::factory('file', array('vfsroot' => '/tmp'));
+if (is_a($vfs, 'PEAR_Error')) {
+    printf("ERROR(1): %s\n", $vfs->getMessage);
+    exit;
+}
+echo "ok\n";
+
+echo "Populating VFS... ";
+$FILES = array('a' => md5(uniqid('a', true)),
+               'c/d' => md5(uniqid('c/d', true)),
+               'e/f' => md5(uniqid('e/f', true)));
+
+foreach ($FILES as $fname => $data) {
+    preg_match('!^(.*)/([^/]*)$!', 'root/' . $fname, $matches);
+    $path = $matches[1];
+    $file = $matches[2];
+    $res = $vfs->writeData($path, $file, $data, true);
+    if (is_a($res, 'PEAR_Error')) {
+        printf("ERROR(1): %s\n", $res->getMessage());
+        exit;
+    }
+}
+echo "ok\n";
+
+echo "Creating ISOWriter... ";
+$iso = &VFS_ISOWriter::factory($vfs, $vfs, array('sourceRoot' => 'root',
+                                                 'targetFile' => 'test.iso'));
+if (is_a($iso, 'PEAR_Error')) {
+    printf("ERROR(1): %s\n", $iso->getMessage());
+    exit;
+}
+echo "ok\n";
+
+echo "Creating ISO Image... ";
+$res = $iso->process();
+if (is_a($res, 'PEAR_Error')) {
+    printf("ERROR(1): %s\n", $res->getMessage());
+    exit;
+}
+if (!file_exists('/tmp/test.iso')) {
+    printf("ERROR(2): /tmp/test.iso does not exist after creating image.\n");
+    exit;
+}
+echo "ok\n";
+
+echo "Checking ISO Image (if possible)... ";
+system("/sbin/modprobe loop >/dev/null 2>&1");
+system("/sbin/losetup /dev/loop3 /tmp/test.iso >/dev/null 2>&1", $ec);
+if ($ec == 0) {
+    if (!@mkdir("/tmp/iso-mount", 0755)) {
+        printf("ERROR(1): mkdir /tmp/iso-mount failed.\n");
+        exit;
+    }
+    system("/bin/mount -t iso9660 /dev/loop3 /tmp/iso-mount >/dev/null", $ec);
+    if ($ec != 0) {
+        @rmdir("/tmp/iso-mount");
+        printf("ERROR(2): mount of ISO image failed.\n");
+        exit;
+    }
+    foreach ($FILES as $fname => $data) {
+        $path = '/tmp/iso-mount/' . $fname;
+        if (!file_exists($path)) {
+            @rmdir("/tmp/iso-mount");
+            printf("ERROR(3): %s: does not exist.\n", $path);
+            exit;
+        }
+        $fh = @fopen($path, 'r');
+        if (!is_resource($fh)) {
+            @rmdir("/tmp/iso-mount");
+            printf("ERROR(4): %s: could not open.\n", $path);
+            exit;
+        }
+        $readData = fread($fh, filesize($path));
+        fclose($fh);
+        if ($data != $readData) {
+            @rmdir("/tmp/iso-mount");
+            printf("ERROR(5): %s: data does not match\n", $path);
+            exit;
+        }
+    }
+    system("/bin/umount /dev/loop3 >/dev/null 2>&1", $ec);
+    system("/sbin/losetup -d /dev/loop3 >/dev/null", $ec);
+    @rmdir("/tmp/iso-mount");
+}
+echo "ok\n";
+
+--EXPECT--
+Load... ok
+Creating VFS... ok
+Populating VFS... ok
+Creating ISOWriter... ok
+Creating ISO Image... ok
+Checking ISO Image (if possible)... ok
diff --git a/framework/VFS_ISOWriter/tests/outputstrategy.phpt b/framework/VFS_ISOWriter/tests/outputstrategy.phpt
new file mode 100644 (file)
index 0000000..b7e948f
--- /dev/null
@@ -0,0 +1,95 @@
+--TEST--
+VFS_ISOWriter_RealOutputStrategy:: and drivers
+--FILE--
+<?php
+
+require_once 'VFS.php';
+require_once 'VFS/file.php';
+require_once dirname(__FILE__) . '/../ISOWriter/RealOutputStrategy.php';
+
+/**
+ * This class is to make a file driver for VFS which isn't treated as a file
+ * driver (strategies detect based on class name).
+ */
+class VFS_notfile extends VFS_file {
+}
+
+echo "Load... ok\n";
+testDirectOutputStrategy();
+testCopyOutputStrategy();
+
+function testDirectOutputStrategy()
+{
+    echo "Testing direct output strategy... ";
+
+    $vfs = &VFS::factory('file', array('vfsroot' => '/tmp'));
+    testOutputStrategy($vfs, 'vfs_isowriter_realoutputstrategy_direct');
+}
+
+function testCopyOutputStrategy()
+{
+    echo "Testing copy output strategy... ";
+
+    $vfs = &new VFS_notfile(array('vfsroot' => '/tmp'));
+    testOutputStrategy($vfs, 'vfs_isowriter_realoutputstrategy_copy');
+}
+
+function testOutputStrategy(&$vfs, $expectClass)
+{
+    if (is_a($vfs, 'PEAR_Error')) {
+        echo "ERROR(1): ", $vfs->getMessage(), "\n";
+        return;
+    }
+
+    $outputStrategy = &VFS_ISOWriter_RealOutputStrategy::factory($vfs, 'foo');
+    if (is_a($outputStrategy, 'PEAR_Error')) {
+        echo "ERROR(2): ", $outputStrategy->getMessage(), "\n";
+        return;
+    }
+
+    if (get_class($outputStrategy) != $expectClass) {
+        printf("ERROR(3): expected class '%s', got class '%s'.\n",
+               $expectClass, get_class($outputStrategy));
+        return;
+    }
+
+    $fn = $outputStrategy->getRealFilename();
+    if (is_a($fn, 'PEAR_Error')) {
+        echo "ERROR(4): ", $fn->getMessage(), "\n";
+        return;
+    }
+
+    $fh = @fopen($fn, 'w');
+    if (!is_resource($fh)) {
+        printf("ERROR(5): could not open '%s' for writing.\n", $fn);
+        return;
+    }
+    $data = md5(uniqid('foobar', true));
+    fputs($fh, $data);
+    fclose($fh);
+
+    $res = $outputStrategy->finished();
+    if (is_a($res, 'PEAR_Error')) {
+        echo "ERROR(6): ", $res->getMessage(), "\n";
+        return;
+    }
+
+    $res = $vfs->read('/', 'foo');
+    if (is_a($res, 'PEAR_Error')) {
+        echo "ERROR(7): ", $res->getMessage(), "\n";
+        return;
+    }
+
+    if ($res != $data) {
+        printf("ERROR(8): file contents differ ('%s' vs. '%s').", $res,
+               $data);
+        return;
+    }
+
+    echo "ok\n";
+}
+
+--EXPECT--
+Load... ok
+Testing direct output strategy... ok
+Testing copy output strategy... ok
diff --git a/framework/XML_WBXML/WBXML.php b/framework/XML_WBXML/WBXML.php
new file mode 100644 (file)
index 0000000..0a64ee7
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+
+/**
+ * Constants are from Binary XML Content Format Specification Version 1.3, 25
+ * July 2001 found at http://www.wapforum.org
+ */
+
+/**
+ * From 7.1 Global Tokens.
+ */
+
+define('XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE', 0);  // 0x00
+define('XML_WBXML_GLOBAL_TOKEN_END', 1);          // 0x01
+define('XML_WBXML_GLOBAL_TOKEN_ENTITY', 2);       // 0x02
+define('XML_WBXML_GLOBAL_TOKEN_STR_I', 3);        // 0x03
+define('XML_WBXML_GLOBAL_TOKEN_LITERAL', 4);      // 0x04
+
+define('XML_WBXML_GLOBAL_TOKEN_EXT_I_0', 64);     // 0x40
+define('XML_WBXML_GLOBAL_TOKEN_EXT_I_1', 65);     // 0x41
+define('XML_WBXML_GLOBAL_TOKEN_EXT_I_2', 66);     // 0x42
+define('XML_WBXML_GLOBAL_TOKEN_PI', 67);          // 0x43
+define('XML_WBXML_GLOBAL_TOKEN_LITERAL_C', 68);   // 0x44
+
+define('XML_WBXML_GLOBAL_TOKEN_EXT_T_0', 128);    // 0x80
+define('XML_WBXML_GLOBAL_TOKEN_EXT_T_1', 129);    // 0x81
+define('XML_WBXML_GLOBAL_TOKEN_EXT_T_2', 130);    // 0x82
+define('XML_WBXML_GLOBAL_TOKEN_STR_T', 131);      // 0x83
+define('XML_WBXML_GLOBAL_TOKEN_LITERAL_A', 132);  // 0x84
+
+define('XML_WBXML_GLOBAL_TOKEN_EXT_0', 192);      // 0xC0
+define('XML_WBXML_GLOBAL_TOKEN_EXT_1', 193);      // 0xC1
+define('XML_WBXML_GLOBAL_TOKEN_EXT_2', 194);      // 0xC2
+define('XML_WBXML_GLOBAL_TOKEN_OPAQUE', 195);     // 0xC3
+define('XML_WBXML_GLOBAL_TOKEN_LITERAL_AC', 196); // 0xC4
+
+/**
+ * Not sure where defined.
+ * ADD CHAPTER
+ */
+define('DPI_DTD_WML_1_0', '-//WAPFORUM//DTD WML 1.0//EN');
+define('DPI_DTD_WTA_1_0', '-//WAPFORUM//DTD WTA 1.0//EN');
+define('DPI_DTD_WML_1_1', '-//WAPFORUM//DTD WML 1.1//EN');
+define('DPI_DTD_SI_1_1', '-//WAPFORUM//DTD SI 1.1//EN');
+define('DPI_DTD_SL_1_0', '-//WAPFORUM//DTD SL 1.0//EN');
+define('DPI_DTD_CO_1_0', '-//WAPFORUM//DTD CO 1.0//EN');
+define('DPI_DTD_CHANNEL_1_1', '-//WAPFORUM//DTD CHANNEL 1.1//EN');
+define('DPI_DTD_WML_1_2', '-//WAPFORUM//DTD WML 1.2//EN');
+define('DPI_DTD_WML_1_3', '-//WAPFORUM//DTD WML 1.3//EN');
+define('DPI_DTD_PROV_1_0', '-//WAPFORUM//DTD PROV 1.0//EN');
+define('DPI_DTD_WTA_WML_1_2', '-//WAPFORUM//DTD WTA-WML 1.2//EN');
+define('DPI_DTD_CHANNEL_1_2', '-//WAPFORUM//DTD CHANNEL 1.2//EN');
+
+define('DPI_DTD_SYNCML_1_0', '-//SYNCML//DTD SyncML 1.0//EN');
+define('DPI_DTD_DEVINF_1_0', '-//SYNCML//DTD DevInf 1.0//EN');
+define('DPI_DTD_METINF_1_0', '-//SYNCML//DTD MetInf 1.0//EN');
+define('DPI_DTD_SYNCML_1_1', '-//SYNCML//DTD SyncML 1.1//EN');
+define('DPI_DTD_DEVINF_1_1', '-//SYNCML//DTD DevInf 1.1//EN');
+define('DPI_DTD_METINF_1_1', '-//SYNCML//DTD MetInf 1.1//EN');
+define('DPI_DTD_SYNCML_1_2', '-//SYNCML//DTD SyncML 1.2//EN');
+define('DPI_DTD_DEVINF_1_2', '-//SYNCML//DTD DevInf 1.2//EN');
+define('DPI_DTD_METINF_1_2', '-//SYNCML//DTD MetInf 1.2//EN');
+
+/**
+ * Only default character encodings from J2SE are currently supported.
+ */
+define('CHARSET_US_ASCII', 'US-ASCII');
+define('CHARSET_ISO_8859_1', 'ISO-8859-1');
+define('CHARSET_UTF_8', 'UTF-8');
+define('CHARSET_UTF_16BE', 'UTF-16BE');
+define('CHARSET_UTF_16LE', 'UTF-16LE');
+define('CHARSET_UTF_16', 'UTF-16');
+
+/**
+ * $Horde: framework/XML_WBXML/WBXML.php,v 1.29 2009/01/06 17:49:59 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML {
+
+    /**
+     * Decoding Multi-byte Integers from Section 5.1
+     *
+     * Use long because it is unsigned.
+     */
+    function MBUInt32ToInt($in, &$pos)
+    {
+        $val = 0;
+
+        do {
+            $b = ord($in[$pos++]);
+            $val <<= 7; // Bitshift left 7 bits.
+            $val += ($b & 127);
+        } while (($b & 128) != 0);
+
+        return $val;
+    }
+
+    /**
+     * Encoding Multi-byte Integers from Section 5.1
+     */
+    function intToMBUInt32(&$out, $i)
+    {
+        if ($i > 268435455) {
+            $bytes0 = 0 | XML_WBXML::getBits(0, $i);
+            $bytes1 = 128 | XML_WBXML::getBits(1, $i);
+            $bytes2 = 128 | XML_WBXML::getBits(2, $i);
+            $bytes3 = 128 | XML_WBXML::getBits(3, $i);
+            $bytes4 = 128 | XML_WBXML::getBits(4, $i);
+
+            $out .= chr($bytes4) . chr($bytes3) . chr($bytes2) . chr($bytes1) . chr($bytes0);
+        } elseif ($i > 2097151) {
+            $bytes0 = 0 | XML_WBXML::getBits(0, $i);
+            $bytes1 = 128 | XML_WBXML::getBits(1, $i);
+            $bytes2 = 128 | XML_WBXML::getBits(2, $i);
+            $bytes3 = 128 | XML_WBXML::getBits(3, $i);
+
+            $out .= chr($bytes3) . chr($bytes2) . chr($bytes1) . chr($bytes0);
+        } elseif ($i > 16383) {
+            $bytes0 = 0 | XML_WBXML::getBits(0, $i);
+            $bytes1 = 128 | XML_WBXML::getBits(1, $i);
+            $bytes2 = 128 | XML_WBXML::getBits(2, $i);
+
+            $out .= chr($bytes2) . chr($bytes1) . chr($bytes0);
+        } elseif ($i > 127) {
+            $bytes0 = 0 | XML_WBXML::getBits(0, $i);
+            $bytes1 = 128 | XML_WBXML::getBits(1, $i);
+
+            $out .= chr($bytes1) . chr($bytes0);
+        } else {
+            $bytes0 = 0 | XML_WBXML::getBits(0, $i);
+
+            $out .= chr($bytes0);
+        }
+    }
+
+    function getBits($num, $l)
+    {
+        switch ($num) {
+        case 0:
+            return $l & 127; // 0x7F
+
+        case 1:
+            return ($l >> 7) & 127; // 0x7F
+
+        case 2:
+            return ($l >> 14) & 127; // 0x7F
+
+        case 3:
+            return ($l >> 21) & 127; // 0x7F
+
+        case 4:
+            return ($l >> 28) & 127; // 0x7F
+        }
+
+        return 0;
+    }
+
+    function getDPIString($i)
+    {
+        /**
+         * ADD CHAPTER
+         */
+        $DPIString = array(2 => DPI_DTD_WML_1_0,
+                           3 => DPI_DTD_WTA_1_0,
+                           4 => DPI_DTD_WML_1_1,
+                           5 => DPI_DTD_SI_1_1,
+                           6 => DPI_DTD_SL_1_0,
+                           7 => DPI_DTD_CO_1_0,
+                           8 => DPI_DTD_CHANNEL_1_1,
+                           9 => DPI_DTD_WML_1_2,
+                           10 => DPI_DTD_WML_1_3,
+                           11 => DPI_DTD_PROV_1_0,
+                           12 => DPI_DTD_WTA_WML_1_2,
+                           13 => DPI_DTD_CHANNEL_1_2,
+
+                           // Not all SyncML clients know this, so we
+                           // should use the string table.
+                           // 0xFD1 => DPI_DTD_SYNCML_1_1,
+                           // These codes are taken from libwbxml wbxml_tables.h:
+                           4049 => DPI_DTD_SYNCML_1_0, // 0x0fd1
+                           4050 => DPI_DTD_DEVINF_1_0, // 0x0fd2
+                           4051 => DPI_DTD_SYNCML_1_1, // 0x0fd3
+                           4052 => DPI_DTD_DEVINF_1_1, // 0x0fd4
+                           4609 => DPI_DTD_SYNCML_1_2, // 0x1201
+                           //@todo: verify this:
+                           4611 => DPI_DTD_DEVINF_1_2  // 0x1203
+// taken from libxml but might be wrong:
+//                           4610 => DPI_DTD_DEVINF_1_2, // 0x1202
+//                           4611 => DPI_DTD_METINF_1_2  // 0x1203
+                           );
+        return isset($DPIString[$i]) ? $DPIString[$i] : null;
+    }
+
+    function getDPIInt($dpi)
+    {
+        /**
+         * ADD CHAPTER
+         */
+        $DPIInt = array(DPI_DTD_WML_1_0 => 2,
+                        DPI_DTD_WTA_1_0 => 3,
+                        DPI_DTD_WML_1_1 => 4,
+                        DPI_DTD_SI_1_1 => 5,
+                        DPI_DTD_SL_1_0 => 6,
+                        DPI_DTD_CO_1_0 => 7,
+                        DPI_DTD_CHANNEL_1_1 => 8,
+                        DPI_DTD_WML_1_2 => 9,
+                        DPI_DTD_WML_1_3 => 10,
+                        DPI_DTD_PROV_1_0 => 11,
+                        DPI_DTD_WTA_WML_1_2 => 12,
+                        DPI_DTD_CHANNEL_1_2 => 13,
+
+                        // Not all SyncML clients know this, so maybe we
+                        // should use the string table.
+                           // These codes are taken from libwbxml wbxml_tables.h:
+                        DPI_DTD_SYNCML_1_0 => 4049,
+                        DPI_DTD_DEVINF_1_0 => 4050,
+                        DPI_DTD_SYNCML_1_1 => 4051,
+                        DPI_DTD_DEVINF_1_1 => 4052,
+                        DPI_DTD_SYNCML_1_2 => 4609, // 0x1201
+//                        DPI_DTD_DEVINF_1_2 => 4610, // 0x1202
+//                        DPI_DTD_METINF_1_2 => 4611  // 0x1203
+                        //@todo: verify this
+                        DPI_DTD_DEVINF_1_2 => 4611  // 0x1203
+                        // DPI_DTD_SYNCML_1_1 => 0xFD1,
+                        // DPI_DTD_DEVINF_1_1 => 0xFD2,
+                        );
+
+        return isset($DPIInt[$dpi]) ? $DPIInt[$dpi] : 0;
+    }
+
+    /**
+     * Returns the character encoding.
+     * only default character encodings from J2SE are supported
+     * from http://www.iana.org/assignments/character-sets
+     * and http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html
+     */
+    function getCharsetString($cs)
+    {
+        /**
+         * From http://www.iana.org/assignments/character-sets
+         */
+        $charsetString = array(3 => 'US-ASCII',
+                               4 => 'ISO-8859-1',
+                               106 => 'UTF-8',
+                               1013 => 'UTF-16BE',
+                               1014 => 'UTF-16LE',
+                               1015 => 'UTF-16');
+
+        return isset($charsetString[$cs]) ? $charsetString[$cs] : null;
+    }
+
+    /**
+     * Returns the character encoding.
+     *
+     * Only default character encodings from J2SE are supported.
+     *
+     * From http://www.iana.org/assignments/character-sets and
+     * http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html
+     */
+    function getCharsetInt($cs)
+    {
+        /**
+         * From http://www.iana.org/assignments/character-sets
+         */
+        $charsetInt = array('US-ASCII' => 3,
+                            'ISO-8859-1' => 4,
+                            'UTF-8' => 106,
+                            'UTF-16BE' => 1013,
+                            'UTF-16LE' => 1014,
+                            'UTF-16' => 1015);
+
+        return isset($charsetInt[$cs]) ? $charsetInt[$cs] : null;
+    }
+
+}
+
+/**
+ * @package XML_WBXML
+ */
+class XML_WBXML_HashTable {
+
+    var $_h;
+
+    function set($k, $v)
+    {
+        $this->_h[$k] = $v;
+    }
+
+    function get($k)
+    {
+        return isset($this->_h[$k]) ? $this->_h[$k] : null;
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/ContentHandler.php b/framework/XML_WBXML/WBXML/ContentHandler.php
new file mode 100644 (file)
index 0000000..78156a9
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/ContentHandler.php,v 1.25 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_ContentHandler {
+
+    var $_currentUri;
+    var $_output = '';
+
+    var $_opaqueHandler;
+
+    /**
+     * Charset.
+     */
+    var $_charset = 'UTF-8';
+
+    /**
+     * WBXML Version.
+     * 1, 2, or 3 supported
+     */
+    var $_wbxmlVersion = 2;
+
+    function XML_WBXML_ContentHandler()
+    {
+        $this->_currentUri = new XML_WBXML_LifoQueue();
+    }
+
+    /**
+     */
+    function raiseError($error)
+    {
+        if (!class_exists('PEAR')) {
+            require 'PEAR.php';
+        }
+        return PEAR::raiseError($error);
+    }
+
+    function getCharsetStr()
+    {
+        return $this->_charset;
+    }
+
+    function setCharset($cs)
+    {
+        $this->_charset = $cs;
+    }
+
+    function getVersion()
+    {
+        return $this->_wbxmlVersion;
+    }
+
+    function setVersion($v)
+    {
+        $this->_wbxmlVersion = 2;
+    }
+
+    function getOutput()
+    {
+        return $this->_output;
+    }
+
+    function getOutputSize()
+    {
+        return strlen($this->_output);
+    }
+
+    function startElement($uri, $element, $attrs = array())
+    {
+        $this->_output .= '<' . $element;
+
+        $currentUri = $this->_currentUri->top();
+
+        if (((!$currentUri) || ($currentUri != $uri)) && $uri) {
+            $this->_output .= ' xmlns="' . $uri . '"';
+        }
+
+        $this->_currentUri->push($uri);
+
+        foreach ($attrs as $attr) {
+            $this->_output .= ' ' . $attr['attribute'] . '="' . $attr['value'] . '"';
+        }
+
+        $this->_output .= '>';
+    }
+
+    function endElement($uri, $element)
+    {
+        $this->_output .= '</' . $element . '>';
+
+        $this->_currentUri->pop();
+    }
+
+    function characters($str)
+    {
+        $this->_output .= $str;
+    }
+
+    function opaque($o)
+    {
+        $this->_output .= $o;
+    }
+
+    function setOpaqueHandler($opaqueHandler)
+    {
+        $this->_opaqueHandler = $opaqueHandler;
+    }
+
+    function removeOpaqueHandler()
+    {
+        unset($this->_opaqueHandler);
+    }
+
+    function createSubHandler()
+    {
+        $name = get_class($this); // clone current class
+        $sh = new $name();
+        $sh->setCharset($this->getCharsetStr());
+        $sh->setVersion($this->getVersion());
+        return $sh;
+    }
+
+}
+
+class XML_WBXML_LifoQueue {
+
+    var $_queue = array();
+
+    function XML_WBXML_LifoQueue()
+    {
+    }
+
+    function push($obj)
+    {
+        $this->_queue[] = $obj;
+    }
+
+    function pop()
+    {
+        if (count($this->_queue)) {
+            return array_pop($this->_queue);
+        } else {
+            return null;
+        }
+    }
+
+    function top()
+    {
+        if ($count = count($this->_queue)) {
+            return $this->_queue[$count - 1];
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/DTD.php b/framework/XML_WBXML/WBXML/DTD.php
new file mode 100644 (file)
index 0000000..1d3b6ed
--- /dev/null
@@ -0,0 +1,155 @@
+<?php
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/DTD.php,v 1.15 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_DTD {
+
+    var $version;
+    var $intTags;
+    var $intAttributes;
+    var $strTags;
+    var $strAttributes;
+    var $intCodePages;
+    var $strCodePages;
+    var $strCodePagesURI;
+    var $URI;
+    var $XMLNS;
+    var $DPI;
+
+    function XML_WBXML_DTD($v)
+    {
+        $this->version = $v;
+        $this->init();
+    }
+
+    function init()
+    {
+    }
+
+    function setAttribute($intAttribute, $strAttribute)
+    {
+        $this->strAttributes[$strAttribute] = $intAttribute;
+        $this->intAttributes[$intAttribute] = $strAttribute;
+    }
+
+    function setTag($intTag, $strTag)
+    {
+        $this->strTags[$strTag] = $intTag;
+        $this->intTags[$intTag] = $strTag;
+    }
+
+    function setCodePage($intCodePage, $strCodePage, $strCodePageURI)
+    {
+        $this->strCodePagesURI[$strCodePageURI] = $intCodePage;
+        $this->strCodePages[$strCodePage] = $intCodePage;
+        $this->intCodePages[$intCodePage] = $strCodePage;
+    }
+
+    function toTagStr($tag)
+    {
+        return isset($this->intTags[$tag]) ? $this->intTags[$tag] : false;
+    }
+
+    function toAttributeStr($attribute)
+    {
+        return isset($this->intTags[$attribute]) ? $this->intTags[$attribute] : false;
+    }
+
+    function toCodePageStr($codePage)
+    {
+        return isset($this->intCodePages[$codePage]) ? $this->intCodePages[$codePage] : false;
+    }
+
+    function toTagInt($tag)
+    {
+        return isset($this->strTags[$tag]) ? $this->strTags[$tag] : false;
+    }
+
+    function toAttributeInt($attribute)
+    {
+        return isset($this->strAttributes[$attribute]) ? $this->strAttributes[$attribute] : false;
+    }
+
+    function toCodePageInt($codePage)
+    {
+        return isset($this->strCodePages[$codePage]) ? $this->strCodePages[$codePage] : false;
+    }
+
+    function toCodePageURI($uri)
+    {
+        $uri = strtolower($uri);
+        if (!isset($this->strCodePagesURI[$uri])) {
+            die("unable to find codepage for $uri!\n");
+        }
+
+        $ret = isset($this->strCodePagesURI[$uri]) ? $this->strCodePagesURI[$uri] : false;
+
+        return $ret;
+    }
+
+    /**
+     * Getter for property version.
+     * @return Value of property version.
+     */
+    function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * Setter for property version.
+     * @param integer $v  New value of property version.
+     */
+    function setVersion($v)
+    {
+        $this->version = $v;
+    }
+
+    /**
+     * Getter for property URI.
+     * @return Value of property URI.
+     */
+    function getURI()
+    {
+        return $this->URI;
+    }
+
+    /**
+     * Setter for property URI.
+     * @param string $u  New value of property URI.
+     */
+    function setURI($u)
+    {
+        $this->URI = $u;
+    }
+
+    /**
+     * Getter for property DPI.
+     * @return Value of property DPI.
+     */
+    function getDPI()
+    {
+        return $this->DPI;
+    }
+
+    /**
+     * Setter for property DPI.
+     * @param DPI New value of property DPI.
+     */
+    function setDPI($d)
+    {
+        $this->DPI = $d;
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/DTD/SyncML.php b/framework/XML_WBXML/WBXML/DTD/SyncML.php
new file mode 100644 (file)
index 0000000..b3732a3
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+include_once 'XML/WBXML/DTD.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/DTD/SyncML.php,v 1.20 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_DTD_SyncML extends XML_WBXML_DTD {
+
+    function init()
+    {
+        /* this code table has been extracted from libwbxml
+         * (see http://libwbxml.aymerick.com/) by using
+         *
+         * grep '\"[^\"]*\", *0x.., 0x.. },' wbxml_tables.c
+         * | sed -e 's#^.*\"\([^\"]*\)\", *\(0x..\), \(0x..\) },.*$#        \$this->setTag\(\3, \"\1\"\); // \2#g'
+         */
+
+        $this->setTag(0x05, "Add"); // 0x00
+        $this->setTag(0x06, "Alert"); // 0x00
+        $this->setTag(0x07, "Archive"); // 0x00
+        $this->setTag(0x08, "Atomic"); // 0x00
+        $this->setTag(0x09, "Chal"); // 0x00
+        $this->setTag(0x0a, "Cmd"); // 0x00
+        $this->setTag(0x0b, "CmdID"); // 0x00
+        $this->setTag(0x0c, "CmdRef"); // 0x00
+        $this->setTag(0x0d, "Copy"); // 0x00
+        $this->setTag(0x0e, "Cred"); // 0x00
+        $this->setTag(0x0f, "Data"); // 0x00
+        $this->setTag(0x10, "Delete"); // 0x00
+        $this->setTag(0x11, "Exec"); // 0x00
+        $this->setTag(0x12, "Final"); // 0x00
+        $this->setTag(0x13, "Get"); // 0x00
+        $this->setTag(0x14, "Item"); // 0x00
+        $this->setTag(0x15, "Lang"); // 0x00
+        $this->setTag(0x16, "LocName"); // 0x00
+        $this->setTag(0x17, "LocURI"); // 0x00
+        $this->setTag(0x18, "Map"); // 0x00
+        $this->setTag(0x19, "MapItem"); // 0x00
+        $this->setTag(0x1a, "Meta"); // 0x00
+        $this->setTag(0x1b, "MsgID"); // 0x00
+        $this->setTag(0x1c, "MsgRef"); // 0x00
+        $this->setTag(0x1d, "NoResp"); // 0x00
+        $this->setTag(0x1e, "NoResults"); // 0x00
+        $this->setTag(0x1f, "Put"); // 0x00
+        $this->setTag(0x20, "Replace"); // 0x00
+        $this->setTag(0x21, "RespURI"); // 0x00
+        $this->setTag(0x22, "Results"); // 0x00
+        $this->setTag(0x23, "Search"); // 0x00
+        $this->setTag(0x24, "Sequence"); // 0x00
+        $this->setTag(0x25, "SessionID"); // 0x00
+        $this->setTag(0x26, "SftDel"); // 0x00
+        $this->setTag(0x27, "Source"); // 0x00
+        $this->setTag(0x28, "SourceRef"); // 0x00
+        $this->setTag(0x29, "Status"); // 0x00
+        $this->setTag(0x2a, "Sync"); // 0x00
+        $this->setTag(0x2b, "SyncBody"); // 0x00
+        $this->setTag(0x2c, "SyncHdr"); // 0x00
+        $this->setTag(0x2d, "SyncML"); // 0x00
+        $this->setTag(0x2e, "Target"); // 0x00
+        $this->setTag(0x2f, "TargetRef"); // 0x00
+        $this->setTag(0x30, "Reserved for future use"); // 0x00
+        $this->setTag(0x31, "VerDTD"); // 0x00
+        $this->setTag(0x32, "VerProto"); // 0x00
+        $this->setTag(0x33, "NumberOfChanges"); // 0x00
+        $this->setTag(0x34, "MoreData"); // 0x00
+        $this->setTag(0x35, "Field"); // 0x00
+        $this->setTag(0x36, "Filter"); // 0x00
+        $this->setTag(0x37, "Record"); // 0x00
+        $this->setTag(0x38, "FilterType"); // 0x00
+        $this->setTag(0x39, "SourceParent"); // 0x00
+        $this->setTag(0x3a, "TargetParent"); // 0x00
+        $this->setTag(0x3b, "Move"); // 0x00
+        $this->setTag(0x3c, "Correlator"); // 0x00
+
+        if ($this->version == 1) {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_1, 'syncml:syncml1.1');
+            $this->setCodePage(1, DPI_DTD_METINF_1_1, 'syncml:metinf1.1');
+            $this->setURI('syncml:syncml1.1');
+        } elseif ($this->version == 2) {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_2, 'syncml:syncml1.2');
+            $this->setCodePage(1, DPI_DTD_METINF_1_2, 'syncml:metinf1.2');
+            $this->setURI('syncml:syncml1.2');
+        } else {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_0, 'syncml:syncml1.0');
+            $this->setCodePage(1, DPI_DTD_METINF_1_0, 'syncml:metinf1.0');
+            $this->setURI('syncml:syncml1.0');
+        }
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/DTD/SyncMLDevInf.php b/framework/XML_WBXML/WBXML/DTD/SyncMLDevInf.php
new file mode 100644 (file)
index 0000000..a47ac52
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+include_once 'XML/WBXML/DTD.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/DTD/SyncMLDevInf.php,v 1.19 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_DTD_SyncMLDevInf extends XML_WBXML_DTD {
+
+    function init()
+    {
+        /* this code table has been extracted from libwbxml
+         * (see http://libwbxml.aymerick.com/) by using
+         *
+         * grep '\"[^\"]*\", *0x.., 0x.. },' wbxml_tables.c
+         * | sed -e 's#^.*\"\([^\"]*\)\", *\(0x..\), \(0x..\) },.*$#        \$this->setTag\(\3, \"\1\"\); // \2#g'
+         */
+
+        $this->setTag(0x05, "CTCap"); // 0x00
+        $this->setTag(0x06, "CTType"); // 0x00
+        $this->setTag(0x07, "DataStore"); // 0x00
+        $this->setTag(0x08, "DataType"); // 0x00
+        $this->setTag(0x09, "DevID"); // 0x00
+        $this->setTag(0x0a, "DevInf"); // 0x00
+        $this->setTag(0x0b, "DevTyp"); // 0x00
+        $this->setTag(0x0c, "DisplayName"); // 0x00
+        $this->setTag(0x0d, "DSMem"); // 0x00
+        $this->setTag(0x0e, "Ext"); // 0x00
+        $this->setTag(0x0f, "FwV"); // 0x00
+        $this->setTag(0x10, "HwV"); // 0x00
+        $this->setTag(0x11, "Man"); // 0x00
+        $this->setTag(0x12, "MaxGUIDSize"); // 0x00
+        $this->setTag(0x13, "MaxID"); // 0x00
+        $this->setTag(0x14, "MaxMem"); // 0x00
+        $this->setTag(0x15, "Mod"); // 0x00
+        $this->setTag(0x16, "OEM"); // 0x00
+        $this->setTag(0x17, "ParamName"); // 0x00
+        $this->setTag(0x18, "PropName"); // 0x00
+        $this->setTag(0x19, "Rx"); // 0x00
+        $this->setTag(0x1a, "Rx-Pref"); // 0x00
+        $this->setTag(0x1b, "SharedMem"); // 0x00
+        $this->setTag(0x1c, "Size"); // 0x00
+        $this->setTag(0x1d, "SourceRef"); // 0x00
+        $this->setTag(0x1e, "SwV"); // 0x00
+        $this->setTag(0x1f, "SyncCap"); // 0x00
+        $this->setTag(0x20, "SyncType"); // 0x00
+        $this->setTag(0x21, "Tx"); // 0x00
+        $this->setTag(0x22, "Tx-Pref"); // 0x00
+        $this->setTag(0x23, "ValEnum"); // 0x00
+        $this->setTag(0x24, "VerCT"); // 0x00
+        $this->setTag(0x25, "VerDTD"); // 0x00
+        $this->setTag(0x26, "XNam"); // 0x00
+        $this->setTag(0x27, "XVal"); // 0x00
+        $this->setTag(0x28, "UTC"); // 0x00
+        $this->setTag(0x29, "SupportNumberOfChanges"); // 0x00
+        $this->setTag(0x2a, "SupportLargeObjs"); // 0x00
+        $this->setTag(0x2b, "Property"); // 0x00
+        $this->setTag(0x2c, "PropParam"); // 0x00
+        $this->setTag(0x2d, "MaxOccur"); // 0x00
+        $this->setTag(0x2e, "NoTruncate"); // 0x00
+        $this->setTag(0x30, "Filter-Rx"); // 0x00
+        $this->setTag(0x31, "FilterCap"); // 0x00
+        $this->setTag(0x32, "FilterKeyword"); // 0x00
+        $this->setTag(0x33, "FieldLevel"); // 0x00
+        $this->setTag(0x34, "SupportHierarchicalSync"); // 0x00
+
+        if ($this->version == 1) {
+            $this->setCodePage(0, DPI_DTD_DEVINF_1_1, 'syncml:devinf1.1');
+            $this->setURI('syncml:devinf1.1');
+        } elseif ($this->version == 2) {
+            $this->setCodePage(0, DPI_DTD_DEVINF_1_2, 'syncml:devinf1.2');
+            $this->setURI('syncml:devinf1.2');
+        } else {
+            $this->setCodePage(0, DPI_DTD_DEVINF_1_0, 'syncml:devinf1.0');
+            $this->setURI('syncml:devinf1.0');
+        }
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/DTD/SyncMLMetInf.php b/framework/XML_WBXML/WBXML/DTD/SyncMLMetInf.php
new file mode 100644 (file)
index 0000000..081569c
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+include_once 'XML/WBXML/DTD.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/DTD/SyncMLMetInf.php,v 1.17 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_DTD_SyncMLMetInf extends XML_WBXML_DTD {
+
+    function init()
+    {
+        /* this code table has been extracted from libwbxml
+         * (see http://libwbxml.aymerick.com/) by using
+         *
+         * grep '\"[^\"]*\", *0x.., 0x.. },' wbxml_tables.c
+         * | sed -e 's#^.*\"\([^\"]*\)\", *\(0x..\), \(0x..\) },.*$#        \$this->setTag\(\3, \"\1\"\); // \2#g'
+         */
+
+        $this->setTag(0x05, "Anchor"); // 0x01
+        $this->setTag(0x06, "EMI"); // 0x01
+        $this->setTag(0x07, "Format"); // 0x01
+        $this->setTag(0x08, "FreeID"); // 0x01
+        $this->setTag(0x09, "FreeMem"); // 0x01
+        $this->setTag(0x0a, "Last"); // 0x01
+        $this->setTag(0x0b, "Mark"); // 0x01
+        $this->setTag(0x0c, "MaxMsgSize"); // 0x01
+        $this->setTag(0x15, "MaxObjSize"); // 0x01
+        $this->setTag(0x0d, "Mem"); // 0x01
+        $this->setTag(0x0e, "MetInf"); // 0x01
+        $this->setTag(0x0f, "Next"); // 0x01
+        $this->setTag(0x10, "NextNonce"); // 0x01
+        $this->setTag(0x11, "SharedMem"); // 0x01
+        $this->setTag(0x12, "Size"); // 0x01
+        $this->setTag(0x13, "Type"); // 0x01
+        $this->setTag(0x14, "Version"); // 0x01
+        $this->setTag(0x15, "MaxObjSize"); // 0x01
+        $this->setTag(0x16, "FieldLevel"); // 0x01
+
+        if ($this->version == 1) {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_1, 'syncml:syncml1.1');
+            $this->setCodePage(1, DPI_DTD_METINF_1_1, 'syncml:metinf1.1');
+            $this->setURI('syncml:metinf1.1');
+            //$this->setURI('syncml:metinf'); // for some funny reason, libwbxml produces no :metinf1.1 here
+        } elseif ($this->version == 2) {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_2, 'syncml:syncml1.2');
+            $this->setCodePage(1, DPI_DTD_METINF_1_2, 'syncml:metinf1.2');
+            $this->setURI('syncml:metinf1.2');
+        } else {
+            $this->setCodePage(0, DPI_DTD_SYNCML_1_0, 'syncml:syncml1.0');
+            $this->setCodePage(1, DPI_DTD_METINF_1_0, 'syncml:metinf1.0');
+            $this->setURI('syncml:metinf1.0');
+        }
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/DTDManager.php b/framework/XML_WBXML/WBXML/DTDManager.php
new file mode 100644 (file)
index 0000000..001b533
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+include_once 'XML/WBXML/DTD/SyncML.php';
+include_once 'XML/WBXML/DTD/SyncMLMetInf.php';
+include_once 'XML/WBXML/DTD/SyncMLDevInf.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/DTDManager.php,v 1.21 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_DTDManager {
+
+    /**
+     * @var array
+     */
+    var $_strDTD = array();
+
+    /**
+     * @var array
+     */
+    var $_strDTDURI = array();
+
+    /**
+     */
+    function XML_WBXML_DTDManager()
+    {
+        $this->registerDTD(DPI_DTD_SYNCML_1_0, 'syncml:syncml1.0', new XML_WBXML_DTD_SyncML(0));
+        $this->registerDTD(DPI_DTD_SYNCML_1_1, 'syncml:syncml1.1', new XML_WBXML_DTD_SyncML(1));
+        $this->registerDTD(DPI_DTD_SYNCML_1_2, 'syncml:syncml1.2', new XML_WBXML_DTD_SyncML(2));
+
+        $this->registerDTD(DPI_DTD_METINF_1_0, 'syncml:metinf1.0', new XML_WBXML_DTD_SyncMLMetInf(0));
+        $this->registerDTD(DPI_DTD_METINF_1_1, 'syncml:metinf1.1', new XML_WBXML_DTD_SyncMLMetInf(1));
+        $this->registerDTD(DPI_DTD_METINF_1_2, 'syncml:metinf1.2', new XML_WBXML_DTD_SyncMLMetInf(2));
+
+        $this->registerDTD(DPI_DTD_DEVINF_1_0, 'syncml:devinf1.0', new XML_WBXML_DTD_SyncMLDevInf(0));
+        $this->registerDTD(DPI_DTD_DEVINF_1_1, 'syncml:devinf1.1', new XML_WBXML_DTD_SyncMLDevInf(1));
+        $this->registerDTD(DPI_DTD_DEVINF_1_2, 'syncml:devinf1.2', new XML_WBXML_DTD_SyncMLDevInf(2));
+    }
+
+    /**
+     */
+    function &getInstance($publicIdentifier)
+    {
+        $publicIdentifier = strtolower($publicIdentifier);
+        if (isset($this->_strDTD[$publicIdentifier])) {
+            $dtd = &$this->_strDTD[$publicIdentifier];
+        } else {
+            $dtd = null;
+        }
+        return $dtd;
+    }
+
+    /**
+     */
+    function &getInstanceURI($uri)
+    {
+        $uri = strtolower($uri);
+
+        // some manual hacks:
+        if ($uri == 'syncml:syncml') {
+            $uri = 'syncml:syncml1.0';
+        }
+        if ($uri == 'syncml:metinf') {
+            $uri = 'syncml:metinf1.0';
+        }
+        if ($uri == 'syncml:devinf') {
+            $uri = 'syncml:devinf1.0';
+        }
+
+        if (isset($this->_strDTDURI[$uri])) {
+            $dtd = &$this->_strDTDURI[$uri];
+        } else {
+            $dtd = null;
+        }
+        return $dtd;
+    }
+
+    /**
+     */
+    function registerDTD($publicIdentifier, $uri, &$dtd)
+    {
+        $dtd->setDPI($publicIdentifier);
+
+        $publicIdentifier = strtolower($publicIdentifier);
+
+        $this->_strDTD[$publicIdentifier] = $dtd;
+        $this->_strDTDURI[strtolower($uri)] = $dtd;
+    }
+
+}
diff --git a/framework/XML_WBXML/WBXML/Decoder.php b/framework/XML_WBXML/WBXML/Decoder.php
new file mode 100644 (file)
index 0000000..996a802
--- /dev/null
@@ -0,0 +1,664 @@
+<?php
+
+include_once 'XML/WBXML.php';
+include_once 'XML/WBXML/DTDManager.php';
+include_once 'XML/WBXML/ContentHandler.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/Decoder.php,v 1.46 2009/07/14 00:25:32 mrubinsk Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_Decoder extends XML_WBXML_ContentHandler {
+
+    /**
+     * Document Public Identifier type
+     * 1 mb_u_int32 well known type
+     * 2 string table
+     * from spec but converted into a string.
+     *
+     * Document Public Identifier
+     * Used with dpiType.
+     */
+    var $_dpi;
+
+    /**
+     * String table as defined in 5.7
+     */
+    var $_stringTable = array();
+
+    /**
+     * Content handler.
+     * Currently just outputs raw XML.
+     */
+    var $_ch;
+
+    var $_tagDTD;
+
+    var $_prevAttributeDTD;
+
+    var $_attributeDTD;
+
+    /**
+     * State variables.
+     */
+    var $_tagStack = array();
+    var $_isAttribute;
+    var $_isData = false;
+
+    var $_error = false;
+
+    /**
+     * The DTD Manager.
+     *
+     * @var XML_WBXML_DTDManager
+     */
+    var $_dtdManager;
+
+    /**
+     * The string position.
+     *
+     * @var integer
+     */
+    var $_strpos;
+
+    /**
+     * Constructor.
+     */
+    function XML_WBXML_Decoder()
+    {
+        $this->_dtdManager = new XML_WBXML_DTDManager();
+    }
+
+    /**
+     * Sets the contentHandler that will receive the output of the
+     * decoding.
+     *
+     * @param XML_WBXML_ContentHandler $ch The contentHandler
+     */
+    function setContentHandler(&$ch)
+    {
+        $this->_ch = &$ch;
+    }
+    /**
+     * Return one byte from the input stream.
+     *
+     * @param string $input  The WBXML input string.
+     */
+    function getByte($input)
+    {
+        return ord($input{$this->_strpos++});
+    }
+
+    /**
+     * Takes a WBXML input document and returns decoded XML.
+     * However the preferred and more effecient method is to
+     * use decode() rather than decodeToString() and have an
+     * appropriate contentHandler deal with the decoded data.
+     *
+     * @param string $wbxml  The WBXML document to decode.
+     *
+     * @return string  The decoded XML document.
+     */
+    function decodeToString($wbxml)
+    {
+        $this->_ch = new XML_WBXML_ContentHandler();
+
+        $r = $this->decode($wbxml);
+        if (is_a($r, 'PEAR_Error')) {
+            return $r;
+        }
+        return $this->_ch->getOutput();
+    }
+
+    /**
+     * Takes a WBXML input document and decodes it.
+     * Decoding result is directly passed to the contentHandler.
+     * A contenthandler must be set using setContentHandler
+     * prior to invocation of this method
+     *
+     * @param string $wbxml  The WBXML document to decode.
+     *
+     * @return mixed  True on success or PEAR_Error.
+     */
+    function decode($wbxml)
+    {
+        $this->_error = false; // reset state
+
+        $this->_strpos = 0;
+
+        if (empty($this->_ch)) {
+            return $this->raiseError('No Contenthandler defined.');
+        }
+
+        // Get Version Number from Section 5.4
+        // version = u_int8
+        // currently 1, 2 or 3
+        $this->_wbxmlVersion = $this->getVersionNumber($wbxml);
+
+        // Get Document Public Idetifier from Section 5.5
+        // publicid = mb_u_int32 | (zero index)
+        // zero = u_int8
+        // Containing the value zero (0)
+        // The actual DPI is determined after the String Table is read.
+        $dpiStruct = $this->getDocumentPublicIdentifier($wbxml);
+
+        // Get Charset from 5.6
+        // charset = mb_u_int32
+        $this->_charset = $this->getCharset($wbxml);
+
+        // Get String Table from 5.7
+        // strb1 = length *byte
+        $this->retrieveStringTable($wbxml);
+
+        // Get Document Public Idetifier from Section 5.5.
+        $this->_dpi = $this->getDocumentPublicIdentifierImpl($dpiStruct['dpiType'],
+                                                             $dpiStruct['dpiNumber'],
+                                                             $this->_stringTable);
+
+        // Now the real fun begins.
+        // From Sections 5.2 and 5.8
+
+
+        // Default content handler.
+        $this->_dtdManager = new XML_WBXML_DTDManager();
+
+        // Get the starting DTD.
+        $this->_tagDTD = $this->_dtdManager->getInstance($this->_dpi);
+
+        if (!$this->_tagDTD) {
+            return $this->raiseError('No DTD found for '
+                             . $this->_dpi . '/'
+                             . $dpiStruct['dpiNumber']);
+        }
+
+        $this->_attributeDTD = $this->_tagDTD;
+
+        while (empty($this->_error) && $this->_strpos < strlen($wbxml)) {
+            $this->_decode($wbxml);
+        }
+        if (!empty($this->_error)) {
+            return $this->_error;
+        }
+        return true;
+    }
+
+    function getVersionNumber($input)
+    {
+        return $this->getByte($input);
+    }
+
+    function getDocumentPublicIdentifier($input)
+    {
+        $i = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+        if ($i == 0) {
+            return array('dpiType' => 2,
+                         'dpiNumber' => $this->getByte($input));
+        } else {
+            return array('dpiType' => 1,
+                         'dpiNumber' => $i);
+        }
+    }
+
+    function getDocumentPublicIdentifierImpl($dpiType, $dpiNumber)
+    {
+        if ($dpiType == 1) {
+            return XML_WBXML::getDPIString($dpiNumber);
+        } else {
+            return $this->getStringTableEntry($dpiNumber);
+        }
+    }
+
+    /**
+     * Returns the character encoding. Only default character
+     * encodings from J2SE are supported.  From
+     * http://www.iana.org/assignments/character-sets and
+     * http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html
+     */
+    function getCharset($input)
+    {
+        $cs = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+        return XML_WBXML::getCharsetString($cs);
+    }
+
+    /**
+     * Retrieves the string table.
+     * The string table consists of an mb_u_int32 length
+     * and then length bytes forming the table.
+     * References to the string table refer to the
+     * starting position of the (null terminated)
+     * string in this table.
+     */
+    function retrieveStringTable($input)
+    {
+        $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+        $this->_stringTable = substr($input, $this->_strpos, $size);
+        $this->_strpos += $size;
+        // print "stringtable($size):" . $this->_stringTable ."\n";
+    }
+
+    function getStringTableEntry($index)
+    {
+        if ($index >= strlen($this->_stringTable)) {
+            $this->_error =
+                $this->raiseError('Invalid offset ' . $index
+                                  . ' value encountered around position '
+                                  . $this->_strpos
+                                  . '. Broken wbxml?');
+            return '';
+        }
+
+        // copy of method termstr but without modification of this->_strpos
+
+        $str = '#'; // must start with nonempty string to allow array access
+
+        $i = 0;
+        $ch = $this->_stringTable[$index++];
+        if (ord($ch) == 0) {
+            return ''; // don't return '#'
+        }
+
+        while (ord($ch) != 0) {
+            $str[$i++] = $ch;
+            if ($index >= strlen($this->_stringTable)) {
+                break;
+            }
+            $ch = $this->_stringTable[$index++];
+        }
+        // print "string table entry: $str\n";
+        return $str;
+
+    }
+
+    function _decode($input)
+    {
+        $token = $this->getByte($input);
+        $str = '';
+
+        // print "position: " . $this->_strpos . " token: " . $token . " str10: " . substr($input, $this->_strpos, 10) . "\n"; // @todo: remove debug output
+
+        switch ($token) {
+        case XML_WBXML_GLOBAL_TOKEN_STR_I:
+            // Section 5.8.4.1
+            $str = $this->termstr($input);
+            $this->_ch->characters($str);
+            // print "str:$str\n"; // @TODO Remove debug code
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_STR_T:
+            // Section 5.8.4.1
+            $x = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+            $str = $this->getStringTableEntry($x);
+            $this->_ch->characters($str);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_EXT_I_0:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_I_1:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_I_2:
+            // Section 5.8.4.2
+            $str = $this->termstr($input);
+            $this->_ch->characters($str);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_EXT_T_0:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_T_1:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_T_2:
+            // Section 5.8.4.2
+            $str = $this->getStringTableEnty(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+            $this->_ch->characters($str);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_EXT_0:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_1:
+        case XML_WBXML_GLOBAL_TOKEN_EXT_2:
+            // Section 5.8.4.2
+            $extension = $this->getByte($input);
+            $this->_ch->characters($extension);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_ENTITY:
+            // Section 5.8.4.3
+            // UCS-4 chracter encoding?
+            $entity = $this->entity(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+
+            $this->_ch->characters('&#' . $entity . ';');
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_PI:
+            // Section 5.8.4.4
+            // throw new IOException
+            // die("WBXML global token processing instruction(PI, " + token + ") is unsupported!\n");
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_LITERAL:
+            // Section 5.8.4.5
+            $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+            $this->parseTag($input, $str, false, false);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_LITERAL_A:
+            // Section 5.8.4.5
+            $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+            $this->parseTag($input, $str, true, false);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_LITERAL_AC:
+            // Section 5.8.4.5
+            $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+            $this->parseTag($input, $string, true, true);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_LITERAL_C:
+            // Section 5.8.4.5
+            $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+            $this->parseTag($input, $str, false, true);
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_OPAQUE:
+            // Section 5.8.4.6
+            $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+            if ($size>0) {
+                $b = substr($input, $this->_strpos, $size);
+                // print "opaque of size $size: ($b)\n"; // @todo remove debug
+                $this->_strpos += $size;
+                // opaque data inside a <data> element may or may not be
+                // a nested wbxml document (for example devinf data).
+                // We find out by checking the first byte of the data: if it's
+                // 1, 2 or 3 we expect it to be the version number of a wbxml
+                // document and thus start a new wbxml decoder instance on it.
+
+                if ($size > 0 && $this->_isData && ord($b) <= 10) {
+                    $decoder = new XML_WBXML_Decoder(true);
+                    $decoder->setContentHandler($this->_ch);
+                    $s = $decoder->decode($b);
+            //                /* // @todo: FIXME currently we can't decode Nokia
+                    // DevInf data. So ignore error for the time beeing.
+                    if (is_a($s, 'PEAR_Error')) {
+                        $this->_error = $s;
+                        return;
+                    }
+                    // */
+                    // $this->_ch->characters($s);
+                } else {
+                    /* normal opaque behaviour: just copy the raw data: */
+                    // print "opaque handled as string=$b\n"; // @todo remove debug
+                    $this->_ch->characters($b);
+                }
+            }
+            // old approach to deal with opaque data inside ContentHandler:
+            // FIXME Opaque is used by SYNCML.  Opaque data that depends on the context
+            // if (contentHandler instanceof OpaqueContentHandler) {
+            //     ((OpaqueContentHandler)contentHandler).opaque(b);
+            // } else {
+            //     String str = new String(b, 0, size, charset);
+            //     char[] chars = str.toCharArray();
+
+            //     contentHandler.characters(chars, 0, chars.length);
+            // }
+
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_END:
+            // Section 5.8.4.7.1
+            $str = $this->endTag();
+            break;
+
+        case XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE:
+            // Section 5.8.4.7.2
+            $codePage = $this->getByte($input);
+            // print "switch to codepage $codePage\n"; // @todo: remove debug code
+            $this->switchElementCodePage($codePage);
+            break;
+
+        default:
+            // Section 5.8.2
+            // Section 5.8.3
+            $hasAttributes = (($token & 0x80) != 0);
+            $hasContent = (($token & 0x40) != 0);
+            $realToken = $token & 0x3F;
+            $str = $this->getTag($realToken);
+
+            // print "element:$str\n"; // @TODO Remove debug code
+            $this->parseTag($input, $str, $hasAttributes, $hasContent);
+
+            if ($realToken == 0x0f) {
+                // store if we're inside a Data tag. This may contain
+                // an additional enclosed wbxml document on which we have
+                // to run a seperate encoder
+                $this->_isData = true;
+            } else {
+                $this->_isData = false;
+            }
+            break;
+        }
+    }
+
+    function parseTag($input, $tag, $hasAttributes, $hasContent)
+    {
+        $attrs = array();
+        if ($hasAttributes) {
+            $attrs = $this->getAttributes($input);
+        }
+
+        $this->_ch->startElement($this->getCurrentURI(), $tag, $attrs);
+
+        if ($hasContent) {
+            // FIXME I forgot what does this does. Not sure if this is
+            // right?
+            $this->_tagStack[] = $tag;
+        } else {
+            $this->_ch->endElement($this->getCurrentURI(), $tag);
+        }
+    }
+
+    function endTag()
+    {
+        if (count($this->_tagStack)) {
+            $tag = array_pop($this->_tagStack);
+        } else {
+            $tag = 'Unknown';
+        }
+
+        $this->_ch->endElement($this->getCurrentURI(), $tag);
+
+        return $tag;
+    }
+
+    function getAttributes($input)
+    {
+        $this->startGetAttributes();
+        $hasMoreAttributes = true;
+
+        $attrs = array();
+        $attr = null;
+        $value = null;
+        $token = null;
+
+        while ($hasMoreAttributes) {
+            $token = $this->getByte($input);
+
+            switch ($token) {
+            // Attribute specified.
+            case XML_WBXML_GLOBAL_TOKEN_LITERAL:
+                // Section 5.8.4.5
+                if (isset($attr)) {
+                    $attrs[] = array('attribute' => $attr,
+                                     'value' => $value);
+                }
+
+                $attr = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+                break;
+
+            // Value specified.
+            case XML_WBXML_GLOBAL_TOKEN_EXT_I_0:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_I_1:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_I_2:
+                // Section 5.8.4.2
+                $value .= $this->termstr($input);
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_EXT_T_0:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_T_1:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_T_2:
+                // Section 5.8.4.2
+                $value .= $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_EXT_0:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_1:
+            case XML_WBXML_GLOBAL_TOKEN_EXT_2:
+                // Section 5.8.4.2
+                $value .= $input[$this->_strpos++];
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_ENTITY:
+                // Section 5.8.4.3
+                $value .= $this->entity(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_STR_I:
+                // Section 5.8.4.1
+                $value .= $this->termstr($input);
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_STR_T:
+                // Section 5.8.4.1
+                $value .= $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos));
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_OPAQUE:
+                // Section 5.8.4.6
+                $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos);
+                $b = substr($input, $this->_strpos, $this->_strpos + $size);
+                $this->_strpos += $size;
+
+                $value .= $b;
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_END:
+                // Section 5.8.4.7.1
+                $hasMoreAttributes = false;
+                if (isset($attr)) {
+                    $attrs[] = array('attribute' => $attr,
+                                     'value' => $value);
+                }
+                break;
+
+            case XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE:
+                // Section 5.8.4.7.2
+                $codePage = $this->getByte($input);
+                if (!$this->_prevAttributeDTD) {
+                    $this->_prevAttributeDTD = $this->_attributeDTD;
+                }
+
+                $this->switchAttributeCodePage($codePage);
+                break;
+
+            default:
+                if ($token > 128) {
+                    if (isset($attr)) {
+                        $attrs[] = array('attribute' => $attr,
+                                         'value' => $value);
+                    }
+                    $attr = $this->_attributeDTD->toAttribute($token);
+                } else {
+                    // Value.
+                    $value .= $this->_attributeDTD->toAttribute($token);
+                }
+                break;
+            }
+        }
+
+        if (!$this->_prevAttributeDTD) {
+            $this->_attributeDTD = $this->_prevAttributeDTD;
+            $this->_prevAttributeDTD = false;
+        }
+
+        $this->stopGetAttributes();
+    }
+
+    function startGetAttributes()
+    {
+        $this->_isAttribute = true;
+    }
+
+    function stopGetAttributes()
+    {
+        $this->_isAttribute = false;
+    }
+
+    function getCurrentURI()
+    {
+        if ($this->_isAttribute) {
+            return $this->_tagDTD->getURI();
+        } else {
+            return $this->_attributeDTD->getURI();
+        }
+    }
+
+    function writeString($str)
+    {
+        $this->_ch->characters($str);
+    }
+
+    function getTag($tag)
+    {
+        // Should know which state it is in.
+        return $this->_tagDTD->toTagStr($tag);
+    }
+
+    function getAttribute($attribute)
+    {
+        // Should know which state it is in.
+        $this->_attributeDTD->toAttributeInt($attribute);
+    }
+
+    function switchElementCodePage($codePage)
+    {
+        $this->_tagDTD = &$this->_dtdManager->getInstance($this->_tagDTD->toCodePageStr($codePage));
+        $this->switchAttributeCodePage($codePage);
+    }
+
+    function switchAttributeCodePage($codePage)
+    {
+        $this->_attributeDTD = &$this->_dtdManager->getInstance($this->_attributeDTD->toCodePageStr($codePage));
+    }
+
+    /**
+     * Return the hex version of the base 10 $entity.
+     */
+    function entity($entity)
+    {
+        return dechex($entity);
+    }
+
+    /**
+     * Reads a null terminated string.
+     */
+    function termstr($input)
+    {
+        $str = '#'; // must start with nonempty string to allow array access
+        $i = 0;
+        $ch = $input[$this->_strpos++];
+        if (ord($ch) == 0) {
+            return ''; // don't return '#'
+        }
+        while (ord($ch) != 0) {
+            $str[$i++] = $ch;
+            $ch = $input[$this->_strpos++];
+        }
+
+        return $str;
+    }
+
+}
+
diff --git a/framework/XML_WBXML/WBXML/Encoder.php b/framework/XML_WBXML/WBXML/Encoder.php
new file mode 100644 (file)
index 0000000..4ed2ae1
--- /dev/null
@@ -0,0 +1,420 @@
+<?php
+
+include_once 'XML/WBXML.php';
+include_once 'XML/WBXML/ContentHandler.php';
+include_once 'XML/WBXML/DTDManager.php';
+
+/**
+ * From Binary XML Content Format Specification Version 1.3, 25 July 2001
+ * found at http://www.wapforum.org
+ *
+ * $Horde: framework/XML_WBXML/WBXML/Encoder.php,v 1.53 2009/01/06 17:50:00 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @package XML_WBXML
+ */
+class XML_WBXML_Encoder extends XML_WBXML_ContentHandler {
+
+    var $_strings = array();
+
+    var $_stringTable;
+
+    var $_hasWrittenHeader = false;
+
+    var $_dtd;
+
+    var $_output = '';
+
+    var $_uris = array();
+
+    var $_uriNums = array();
+
+    var $_currentURI;
+
+    var $_subParser = null;
+    var $_subParserStack = 0;
+
+    /**
+     * The XML parser.
+     *
+     * @var resource
+     */
+    var $_parser;
+
+    /**
+     * The DTD Manager.
+     *
+     * @var XML_WBXML_DTDManager
+     */
+    var $_dtdManager;
+
+    /**
+     * Constructor.
+     */
+    function XML_WBXML_Encoder()
+    {
+        $this->_dtdManager = &new XML_WBXML_DTDManager();
+        $this->_stringTable = &new XML_WBXML_HashTable();
+    }
+
+    /**
+     * Take the input $xml and turn it into WBXML. This is _not_ the
+     * intended way of using this class. It is derived from
+     * Contenthandler and one should use it as a ContentHandler and
+     * produce the XML-structure with startElement(), endElement(),
+     * and characters().
+     */
+    function encode($xml)
+    {
+        // Create the XML parser and set method references.
+        $this->_parser = xml_parser_create_ns($this->_charset);
+        xml_set_object($this->_parser, $this);
+        xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
+        xml_set_element_handler($this->_parser, '_startElement', '_endElement');
+        xml_set_character_data_handler($this->_parser, '_characters');
+        xml_set_processing_instruction_handler($this->_parser, '');
+        xml_set_external_entity_ref_handler($this->_parser, '');
+
+        if (!xml_parse($this->_parser, $xml)) {
+            return $this->raiseError(sprintf('XML error: %s at line %d',
+                                             xml_error_string(xml_get_error_code($this->_parser)),
+                                             xml_get_current_line_number($this->_parser)));
+        }
+
+        xml_parser_free($this->_parser);
+
+        return $this->_output;
+    }
+
+    /**
+     * This will write the correct headers.
+     */
+    function writeHeader($uri)
+    {
+        $this->_dtd = &$this->_dtdManager->getInstanceURI($uri);
+        if (!$this->_dtd) {
+            // TODO: proper error handling
+            die('Unable to find dtd for ' . $uri);
+        }
+        $dpiString = $this->_dtd->getDPI();
+
+        // Set Version Number from Section 5.4
+        // version = u_int8
+        // currently 1, 2 or 3
+        $this->writeVersionNumber($this->_wbxmlVersion);
+
+        // Set Document Public Idetifier from Section 5.5
+        // publicid = mb_u_int32 | ( zero index )
+        // zero = u_int8
+        // containing the value zero (0)
+        // The actual DPI is determined after the String Table is read.
+        $this->writeDocumentPublicIdentifier($dpiString, $this->_strings);
+
+        // Set Charset from 5.6
+        // charset = mb_u_int32
+        $this->writeCharset($this->_charset);
+
+        // Set String Table from 5.7
+        // strb1 = length *byte
+        $this->writeStringTable($this->_strings, $this->_charset, $this->_stringTable);
+
+        $this->_currentURI = $uri;
+
+        $this->_hasWrittenHeader = true;
+    }
+
+    function writeVersionNumber($version)
+    {
+        $this->_output .= chr($version);
+    }
+
+    function writeDocumentPublicIdentifier($dpiString, &$strings)
+    {
+        $i = 0;
+
+        // The OMA test suite doesn't like DPI as integer code.
+        // So don't try lookup and always send full DPI string.
+        // $i = XML_WBXML::getDPIInt($dpiString);
+
+        if ($i == 0) {
+            $strings[0] = $dpiString;
+            $this->_output .= chr(0);
+            $this->_output .= chr(0);
+        } else {
+            XML_WBXML::intToMBUInt32($this->_output, $i);
+        }
+    }
+
+    function writeCharset($charset)
+    {
+        $cs = XML_WBXML::getCharsetInt($charset);
+
+        if ($cs == 0) {
+            return $this->raiseError('Unsupported Charset: ' . $charset);
+        } else {
+            XML_WBXML::intToMBUInt32($this->_output, $cs);
+        }
+    }
+
+    function writeStringTable($strings, $charset, $stringTable)
+    {
+        $stringBytes = array();
+        $count = 0;
+        foreach ($strings as $str) {
+            $bytes = $this->_getBytes($str, $charset);
+            $stringBytes = array_merge($stringBytes, $bytes);
+            $nullLength = $this->_addNullByte($bytes);
+            $this->_stringTable->set($str, $count);
+            $count += count($bytes) + $nullLength;
+        }
+
+        XML_WBXML::intToMBUInt32($this->_output, count($stringBytes));
+        $this->_output .= implode('', $stringBytes);
+    }
+
+    function writeString($str, $cs)
+    {
+        $bytes = $this->_getBytes($str, $cs);
+        $this->_output .= implode('', $bytes);
+        $this->writeNull($cs);
+    }
+
+    function writeNull($charset)
+    {
+        $this->_output .= chr(0);
+        return 1;
+    }
+
+    function _addNullByte(&$bytes)
+    {
+        $bytes[] = chr(0);
+        return 1;
+    }
+
+    function _getBytes($string, $cs)
+    {
+        $nbytes = strlen($string);
+
+        $bytes = array();
+        for ($i = 0; $i < $nbytes; $i++) {
+            $bytes[] = $string{$i};
+        }
+
+        return $bytes;
+    }
+
+    function _splitURI($tag)
+    {
+        $parts = explode(':', $tag);
+        $name = array_pop($parts);
+        $uri = implode(':', $parts);
+        return array($uri, $name);
+    }
+
+    function startElement($uri, $name, $attributes = array())
+    {
+        if ($this->_subParser == null) {
+            if (!$this->_hasWrittenHeader) {
+                $this->writeHeader($uri);
+            }
+            if ($this->_currentURI != $uri) {
+                $this->changecodepage($uri);
+            }
+            if ($this->_subParser == null) {
+                $this->writeTag($name, $attributes, true, $this->_charset);
+            } else {
+                $this->_subParser->startElement($uri, $name, $attributes);
+            }
+        } else {
+            $this->_subParserStack++;
+            $this->_subParser->startElement($uri, $name, $attributes);
+        }
+    }
+
+    function _startElement($parser, $tag, $attributes)
+    {
+        list($uri, $name) = $this->_splitURI($tag);
+
+        $this->startElement($uri, $name, $attributes);
+    }
+
+    function opaque($o)
+    {
+        $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_OPAQUE);
+        XML_WBXML::intToMBUInt32($this->_output, strlen($o));
+        $this->_output .= $o;
+    }
+
+    function characters($chars)
+    {
+        $chars = trim($chars);
+
+        if (strlen($chars)) {
+            /* We definitely don't want any whitespace. */
+            if ($this->_subParser == null) {
+                $i = $this->_stringTable->get($chars);
+                if ($i != null) {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_STR_T);
+                    XML_WBXML::intToMBUInt32($this->_output, $i);
+                } else {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_STR_I);
+                    $this->writeString($chars, $this->_charset);
+                }
+            } else {
+                $this->_subParser->characters($chars);
+            }
+        }
+    }
+
+    function _characters($parser, $chars)
+    {
+        $this->characters($chars);
+    }
+
+    function writeTag($name, $attrs, $hasContent, $cs)
+    {
+        if ($attrs != null && !count($attrs)) {
+            $attrs = null;
+        }
+
+        $t = $this->_dtd->toTagInt($name);
+        if ($t == -1) {
+            $i = $this->_stringTable->get($name);
+            if ($i == null) {
+                return $this->raiseError($name . ' is not found in String Table or DTD');
+            } else {
+                if ($attrs == null && !$hasContent) {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_LITERAL);
+                } elseif ($attrs == null && $hasContent) {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_LITERAL_A);
+                } elseif ($attrs != null && $hasContent) {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_LITERAL_C);
+                } elseif ($attrs != null && !$hasContent) {
+                    $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_LITERAL_AC);
+                }
+
+                XML_WBXML::intToMBUInt32($this->_output, $i);
+            }
+        } else {
+            if ($attrs == null && !$hasContent) {
+                $this->_output .= chr($t);
+            } elseif ($attrs == null && $hasContent) {
+                $this->_output .= chr($t | 64);
+            } elseif ($attrs != null && $hasContent) {
+                $this->_output .= chr($t | 128);
+            } elseif ($attrs != null && !$hasContent) {
+                $this->_output .= chr($t | 192);
+            }
+        }
+
+        if ($attrs != null && is_array($attrs) && count($attrs) > 0) {
+            $this->writeAttributes($attrs, $cs);
+        }
+    }
+
+    function writeAttributes($attrs, $cs)
+    {
+        foreach ($attrs as $name => $value) {
+            $this->writeAttribute($name, $value, $cs);
+        }
+
+        $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_END);
+    }
+
+    function writeAttribute($name, $value, $cs)
+    {
+        $a = $this->_dtd->toAttribute($name);
+        if ($a == -1) {
+            $i = $this->_stringTable->get($name);
+            if ($i == null) {
+                return $this->raiseError($name . ' is not found in String Table or DTD');
+            } else {
+                $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_LITERAL);
+                XML_WBXML::intToMBUInt32($this->_output, $i);
+            }
+        } else {
+            $this->_output .= $a;
+        }
+
+        $i = $this->_stringTable->get($name);
+        if ($i != null) {
+            $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_STR_T);
+            XML_WBXML::intToMBUInt32($this->_output, $i);
+        } else {
+            $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_STR_I);
+            $this->writeString($value, $cs);
+        }
+    }
+
+    function endElement($uri, $name)
+    {
+        if ($this->_subParser == null) {
+            $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_END);
+        } else {
+            $this->_subParser->endElement($uri, $name);
+            $this->_subParserStack--;
+
+            if ($this->_subParserStack == 0) {
+                $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_OPAQUE);
+
+                XML_WBXML::intToMBUInt32($this->_output,
+                                         strlen($this->_subParser->getOutput()));
+                $this->_output .= $this->_subParser->getOutput();
+
+                $this->_subParser = null;
+            }
+        }
+    }
+
+    function _endElement($parser, $tag)
+    {
+        list($uri, $name) = $this->_splitURI($tag);
+        $this->endElement($uri, $name);
+    }
+
+    function changecodepage($uri)
+    {
+        // @todo: this is a hack!
+        if ($this->_dtd->getVersion() == 2 && !preg_match('/1\.2$/', $uri)) {
+            $uri .= '1.2';
+        }
+        if ($this->_dtd->getVersion() == 1 && !preg_match('/1\.1$/', $uri)) {
+            $uri .= '1.1';
+        }
+        if ($this->_dtd->getVersion() == 0 && !preg_match('/1\.0$/', $uri)) {
+            $uri .= '1.0';
+        }
+
+        $cp = $this->_dtd->toCodePageURI($uri);
+        if (strlen($cp)) {
+            $this->_dtd = &$this->_dtdManager->getInstanceURI($uri);
+
+            $this->_output .= chr(XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE);
+            $this->_output .= chr($cp);
+            $this->_currentURI = $uri;
+
+        } else {
+            $this->_subParser = &new XML_WBXML_Encoder(true);
+            $this->_subParserStack = 1;
+        }
+    }
+
+    /**
+     * Getter for property output.
+     */
+    function getOutput()
+    {
+        return $this->_output;
+    }
+
+    function getOutputSize()
+    {
+        return strlen($this->_output);
+    }
+
+}
diff --git a/framework/XML_WBXML/docs/examples/MotorolaA780.wbxml b/framework/XML_WBXML/docs/examples/MotorolaA780.wbxml
new file mode 100755 (executable)
index 0000000..fed472d
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/MotorolaA780.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/devinf.wbxml b/framework/XML_WBXML/docs/examples/devinf.wbxml
new file mode 100644 (file)
index 0000000..fc88a90
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/devinf.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/k500i_client_0.wbxml b/framework/XML_WBXML/docs/examples/k500i_client_0.wbxml
new file mode 100644 (file)
index 0000000..21c048a
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/k500i_client_0.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/k700i_syncml_client_0.wbxml b/framework/XML_WBXML/docs/examples/k700i_syncml_client_0.wbxml
new file mode 100644 (file)
index 0000000..e464edc
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/k700i_syncml_client_0.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/syncml_client_packet_1.xml b/framework/XML_WBXML/docs/examples/syncml_client_packet_1.xml
new file mode 100644 (file)
index 0000000..6c4d261
--- /dev/null
@@ -0,0 +1,242 @@
+<?xml version="1.0"?>
+<!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd">
+<SyncML xmlns="syncml:SYNCML1.1">
+ <SyncHdr>
+  <VerDTD>
+   1.1
+  </VerDTD>
+  <VerProto>
+   SyncML/1.1
+  </VerProto>
+  <SessionID>
+   0
+  </SessionID>
+  <MsgID>
+   1
+  </MsgID>
+  <Target>
+   <LocURI>
+    http://pyramid6.com:8080/sync
+   </LocURI>
+  </Target>
+  <Source>
+   <LocURI>
+    AA712857A16E02800
+   </LocURI>
+   <LocName>
+    anthony
+   </LocName>
+  </Source>
+  <Cred>
+   <Meta>
+    <Format xmlns="syncml:metinf">
+     b64
+    </Format>
+    <Type xmlns="syncml:metinf">
+     syncml:auth-md5
+    </Type>
+   </Meta>
+   <Data>
+    amO5sygiWy+nqxA2r0++BA==
+   </Data>
+  </Cred>
+  <Meta>
+   <MaxMsgSize xmlns="syncml:metinf">
+    10000
+   </MaxMsgSize>
+   <MaxObjSize xmlns="syncml:metinf">
+    4000000
+   </MaxObjSize>
+  </Meta>
+ </SyncHdr>
+ <SyncBody>
+  <Put>
+   <CmdID>
+    1
+   </CmdID>
+   <Meta>
+    <Type xmlns="syncml:metinf">
+     application/vnd.syncml-devinf+xml
+    </Type>
+   </Meta>
+   <Item>
+    <Source>
+     <LocURI>
+      ./devinf11
+     </LocURI>
+    </Source>
+    <Data>
+     <DevInf xmlns="syncml:devinf">
+      <VerDTD>
+       1.1
+      </VerDTD>
+      <Man>
+       Synthesis AG
+      </Man>
+      <Mod>
+       Synthesis Sync Client 1.1 PocketPC Std
+      </Mod>
+      <OEM>
+       Synthesis AG
+      </OEM>
+      <SwV>
+       2.0.1.9
+      </SwV>
+      <DevID>
+       SySync Client PocketPC Std
+      </DevID>
+      <DevTyp>
+       workstation
+      </DevTyp>
+      <UTC>
+      </UTC>
+      <SupportNumberOfChanges>
+      </SupportNumberOfChanges>
+      <SupportLargeObjs>
+      </SupportLargeObjs>
+      <DataStore>
+       <SourceRef>
+        ./contacts
+       </SourceRef>
+       <MaxGUIDSize>
+        64
+       </MaxGUIDSize>
+       <Rx-Pref>
+        <CTType>
+         text/x-vcard
+        </CTType>
+        <VerCT>
+         2.1
+        </VerCT>
+       </Rx-Pref>
+       <Tx-Pref>
+        <CTType>
+         text/x-vcard
+        </CTType>
+        <VerCT>
+         2.1
+        </VerCT>
+       </Tx-Pref>
+       <SyncCap>
+        <SyncType>
+         1
+        </SyncType>
+        <SyncType>
+         2
+        </SyncType>
+        <SyncType>
+         3
+        </SyncType>
+        <SyncType>
+         4
+        </SyncType>
+        <SyncType>
+         5
+        </SyncType>
+        <SyncType>
+         6
+        </SyncType>
+       </SyncCap>
+      </DataStore>
+      <CTCap>
+       <CTType>
+        text/x-vcard
+       </CTType>
+       <PropName>
+        BEGIN
+       </PropName>
+       <ValEnum>
+        VCARD
+       </ValEnum>
+       <PropName>
+        END
+       </PropName>
+       <ValEnum>
+        VCARD
+       </ValEnum>
+       <PropName>
+        VERSION
+       </PropName>
+       <ValEnum>
+        2.1
+       </ValEnum>
+       <PropName>
+        N
+       </PropName>
+       <PropName>
+        TITLE
+       </PropName>
+       <PropName>
+        ORG
+       </PropName>
+       <PropName>
+        BDAY
+       </PropName>
+       <PropName>
+        TEL
+       </PropName>
+       <PropName>
+        EMAIL
+       </PropName>
+       <PropName>
+        URL
+       </PropName>
+       <PropName>
+        NOTE
+       </PropName>
+       <PropName>
+        ADR
+       </PropName>
+      </CTCap>
+     </DevInf>
+    </Data>
+   </Item>
+  </Put>
+  <Get>
+   <CmdID>
+    2
+   </CmdID>
+   <Meta>
+    <Type xmlns="syncml:metinf">
+     application/vnd.syncml-devinf+xml
+    </Type>
+   </Meta>
+   <Item>
+    <Target>
+     <LocURI>
+      ./devinf11
+     </LocURI>
+    </Target>
+   </Item>
+  </Get>
+  <Alert>
+   <CmdID>
+    3
+   </CmdID>
+   <Data>
+    201
+   </Data>
+   <Item>
+    <Target>
+     <LocURI>
+      contacts
+     </LocURI>
+    </Target>
+    <Source>
+     <LocURI>
+      ./contacts
+     </LocURI>
+    </Source>
+    <Meta>
+     <Anchor xmlns="syncml:metinf">
+      <Next>
+       20030925T175524Z
+      </Next>
+     </Anchor>
+    </Meta>
+   </Item>
+  </Alert>
+  <Final>
+  </Final>
+ </SyncBody>
+</SyncML>
diff --git a/framework/XML_WBXML/docs/examples/syncml_client_packet_2.xml b/framework/XML_WBXML/docs/examples/syncml_client_packet_2.xml
new file mode 100644 (file)
index 0000000..ad6e3d7
--- /dev/null
@@ -0,0 +1,119 @@
+<?xml version="1.0"?>
+<!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd">
+<SyncML xmlns="syncml:syncml1.1">
+ <SyncHdr>
+  <VerDTD>
+   1.1
+  </VerDTD>
+  <VerProto>
+   SyncML/1.1
+  </VerProto>
+  <SessionID>
+   0
+  </SessionID>
+  <MsgID>
+   1
+  </MsgID>
+  <Target>
+   <LocURI>
+    http://pyramid6.com:8080/sync
+   </LocURI>
+  </Target>
+  <Source>
+   <LocURI>
+    AA712857A16E02800
+   </LocURI>
+   <LocName>
+    anthony
+   </LocName>
+  </Source>
+  <Cred>
+   <Meta>
+    <Format xmlns="syncml:metinf1.1">
+     b64
+    </Format>
+    <Type xmlns="syncml:metinf1.1">
+     syncml:auth-md5
+    </Type>
+   </Meta>
+   <Data>
+    amO5sygiWy+nqxA2r0++BA==
+   </Data>
+  </Cred>
+  <Meta>
+   <MaxMsgSize xmlns="syncml:metinf1.1">
+    10000
+   </MaxMsgSize>
+   <MaxObjSize xmlns="syncml:metinf1.1">
+    4000000
+   </MaxObjSize>
+  </Meta>
+ </SyncHdr>
+ <SyncBody>
+  <Put>
+   <CmdID>
+    1
+   </CmdID>
+   <Meta>
+    <Type xmlns="syncml:metinf1.1">
+     application/vnd.syncml-devinf+xml
+    </Type>
+   </Meta>
+   <Item>
+    <Source>
+     <LocURI>
+      ./devinf11
+     </LocURI>
+    </Source>
+    <Data>
+    </Data>
+   </Item>
+  </Put>
+  <Get>
+   <CmdID>
+    2
+   </CmdID>
+   <Meta>
+    <Type xmlns="syncml:metinf1.1">
+     application/vnd.syncml-devinf+xml
+    </Type>
+   </Meta>
+   <Item>
+    <Target>
+     <LocURI>
+      ./devinf11
+     </LocURI>
+    </Target>
+   </Item>
+  </Get>
+  <Alert>
+   <CmdID>
+    3
+   </CmdID>
+   <Data>
+    201
+   </Data>
+   <Item>
+    <Target>
+     <LocURI>
+      contacts
+     </LocURI>
+    </Target>
+    <Source>
+     <LocURI>
+      ./contacts
+     </LocURI>
+    </Source>
+    <Meta>
+     <Anchor xmlns="syncml:metinf1.1">
+      <Next>
+       20030925T175524Z
+      </Next>
+     </Anchor>
+    </Meta>
+   </Item>
+  </Alert>
+  <Final>
+  </Final>
+ </SyncBody>
+</SyncML>
diff --git a/framework/XML_WBXML/docs/examples/syncml_client_packet_3.wbxml b/framework/XML_WBXML/docs/examples/syncml_client_packet_3.wbxml
new file mode 100644 (file)
index 0000000..e0c55f5
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/syncml_client_packet_3.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/syncml_client_packet_4.wbxml b/framework/XML_WBXML/docs/examples/syncml_client_packet_4.wbxml
new file mode 100644 (file)
index 0000000..1a436c0
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/syncml_client_packet_4.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/syncml_client_packet_5.wbxml b/framework/XML_WBXML/docs/examples/syncml_client_packet_5.wbxml
new file mode 100644 (file)
index 0000000..b93fe76
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/syncml_client_packet_5.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/syncml_server_packet_1.wbxml b/framework/XML_WBXML/docs/examples/syncml_server_packet_1.wbxml
new file mode 100644 (file)
index 0000000..3e9457f
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/syncml_server_packet_1.wbxml differ
diff --git a/framework/XML_WBXML/docs/examples/syncml_server_packet_1.xml b/framework/XML_WBXML/docs/examples/syncml_server_packet_1.xml
new file mode 100644 (file)
index 0000000..0624536
--- /dev/null
@@ -0,0 +1,130 @@
+<?xml version="1.0"?>
+<!DOCTYPE SyncML PUBLIC "-//SYNCML//DTD SyncML 1.1//EN" "http://www.openmobilealliance.org/syncml/docs/syncml_represent_v11_20020215.dtd">
+<SyncML xmlns="syncml:SYNCML1.1">
+ <SyncHdr>
+  <VerDTD>
+   1.1
+  </VerDTD>
+  <VerProto>
+   SyncML/1.1
+  </VerProto>
+  <SessionID>
+   0
+  </SessionID>
+  <MsgID>
+   1
+  </MsgID>
+  <Target>
+   <LocURI>
+    AA712857A16E02800
+   </LocURI>
+   <LocName>
+    anthony
+   </LocName>
+  </Target>
+  <Source>
+   <LocURI>
+    http://pyramid6.com:8080/sync
+   </LocURI>
+  </Source>
+  <Meta>
+   <MaxMsgSize xmlns="syncml:metinf">
+    50000
+   </MaxMsgSize>
+   <MaxObjSize xmlns="syncml:metinf">
+    4000000
+   </MaxObjSize>
+  </Meta>
+ </SyncHdr>
+ <SyncBody>
+  <Status>
+   <CmdID>
+    1
+   </CmdID>
+   <MsgRef>
+    1
+   </MsgRef>
+   <CmdRef>
+    0
+   </CmdRef>
+   <Cmd>
+    SyncHdr
+   </Cmd>
+   <TargetRef>
+    http://pyramid6.com:8080/sync
+   </TargetRef>
+   <SourceRef>
+    AA712857A16E02800
+   </SourceRef>
+   <Chal>
+    <Meta>
+     <Format xmlns="syncml:metinf">
+      b64
+     </Format>
+     <Type xmlns="syncml:metinf">
+      syncml:auth-md5
+     </Type>
+     <NextNonce xmlns="syncml:metinf">
+      O0JUQVAkISE=
+     </NextNonce>
+    </Meta>
+   </Chal>
+   <Data>
+    401
+   </Data>
+  </Status>
+  <Status>
+   <CmdID>
+    2
+   </CmdID>
+   <MsgRef>
+    1
+   </MsgRef>
+   <CmdRef>
+    1
+   </CmdRef>
+   <Cmd>
+    Put
+   </Cmd>
+   <Data>
+    401
+   </Data>
+  </Status>
+  <Status>
+   <CmdID>
+    3
+   </CmdID>
+   <MsgRef>
+    1
+   </MsgRef>
+   <CmdRef>
+    2
+   </CmdRef>
+   <Cmd>
+    Get
+   </Cmd>
+   <Data>
+    401
+   </Data>
+  </Status>
+  <Status>
+   <CmdID>
+    4
+   </CmdID>
+   <MsgRef>
+    1
+   </MsgRef>
+   <CmdRef>
+    3
+   </CmdRef>
+   <Cmd>
+    Alert
+   </Cmd>
+   <Data>
+    401
+   </Data>
+  </Status>
+  <Final>
+  </Final>
+ </SyncBody>
+</SyncML>
diff --git a/framework/XML_WBXML/docs/examples/z600_client_0.wbxml b/framework/XML_WBXML/docs/examples/z600_client_0.wbxml
new file mode 100644 (file)
index 0000000..1afba1f
Binary files /dev/null and b/framework/XML_WBXML/docs/examples/z600_client_0.wbxml differ
diff --git a/framework/XML_WBXML/package.xml b/framework/XML_WBXML/package.xml
new file mode 100644 (file)
index 0000000..d7f1d1a
--- /dev/null
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>XML_WBXML</name>
+ <channel>pear.horde.org</channel>
+ <summary>XML_WBXML:: provides an API for encoding and decoding WBXML documents used in SyncML and other wireless applications.</summary>
+ <description>This package provides encoding and decoding of WBXML (Wireless Binary XML) documents. WBXML is used in SyncML for transferring smaller amounts of data with wireless devices.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Karsten Fourmont</name>
+  <user>karsten</user>
+  <email>karsten@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-09</date>
+ <time>00:10:48</time>
+ <version>
+  <release>0.5.1</release>
+  <api>0.5.1</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="docs">
+    <dir name="examples">
+     <file baseinstalldir="/XML" name="devinf.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="k500i_client_0.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="k700i_syncml_client_0.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_client_packet_1.xml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_client_packet_2.xml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_client_packet_3.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_client_packet_4.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_client_packet_5.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_server_packet_1.wbxml" role="doc" />
+     <file baseinstalldir="/XML" name="syncml_server_packet_1.xml" role="doc" />
+     <file baseinstalldir="/XML" name="z600_client_0.wbxml" role="doc" />
+    </dir> <!-- /docs/examples -->
+   </dir> <!-- /docs -->
+   <dir name="tests">
+    <file baseinstalldir="/XML" name="decode.php" role="test" />
+   </dir> <!-- /tests -->
+   <dir name="WBXML">
+    <dir name="DTD">
+     <file baseinstalldir="/XML" name="SyncML.php" role="php" />
+     <file baseinstalldir="/XML" name="SyncMLDevInf.php" role="php" />
+     <file baseinstalldir="/XML" name="SyncMLMetInf.php" role="php" />
+    </dir> <!-- /WBXML/DTD -->
+    <file baseinstalldir="/XML" name="ContentHandler.php" role="php" />
+    <file baseinstalldir="/XML" name="Decoder.php" role="php" />
+    <file baseinstalldir="/XML" name="DTD.php" role="php" />
+    <file baseinstalldir="/XML" name="DTDManager.php" role="php" />
+    <file baseinstalldir="/XML" name="Encoder.php" role="php" />
+   </dir> <!-- /WBXML -->
+   <file baseinstalldir="/XML" name="WBXML.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.5</release>
+    <api>0.5</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2005-09-21</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release working without external libwbxml support.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-11-25</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/XML_WBXML/tests/decode.php b/framework/XML_WBXML/tests/decode.php
new file mode 100755 (executable)
index 0000000..45e9c5b
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/php
+<?php
+/**
+ * Tests all .wbxml files in ../docs/examples/ by decoding them and
+ * comparing to wbxml2xml's output.
+ *
+ * If filenames are provided on the command line they are used instead
+ * of the complete examples/ directory.
+ *
+ * Errors are displayed as long single lines of XML files. View with
+ * line wrapping turned off to see differences.
+ *
+ * @package XML_WBXML
+ */
+
+if (!is_executable('/usr/bin/wbxml2xml')) {
+    die("/usr/bin/wbxml2xml is required for comparison tests.\n");
+}
+
+include_once dirname(__FILE__) . '/../WBXML/Decoder.php';
+$decoder = new XML_WBXML_Decoder();
+
+$files = array();
+if ($argc > 1) {
+    for ($i = 1; $i <= $argc; ++$i) {
+        if (is_readable($argv[$i])) {
+            $files[] = $argv[$i];
+        }
+    }
+} else {
+    $dir = dirname(__FILE__) . '/../docs/examples/';
+    $d = dir($dir);
+    while (false !== ($entry = $d->read())) {
+        if (preg_match('/\.wbxml$/', $entry)) {
+            $files[] = $dir . $entry;
+        }
+    }
+    $d->close();
+}
+
+foreach ($files as $file) {
+    $xml_ref = shell_exec('/usr/bin/wbxml2xml' . ' -m 0 -o - "' . $file . '" 2>/dev/null');
+    $xml_ref = preg_replace(
+        array(
+            // Ignore <?xml and <!DOCTYPE stuff:
+            '/<\?xml version=\"1\.0\"\?><!DOCTYPE [^>]*>/',
+            // Normalize empty tags.
+            '|<([^>]+)/>|'),
+        array('', '<$1></$1>'),
+        $xml_ref);
+
+    $wbxml_in = file_get_contents($file, 'rb');
+
+    $xml = $decoder->decodeToString($wbxml_in);
+
+    if (is_string($xml)) {
+        // Ignore <?xml and <!DOCTYPE stuff.
+        $xml = preg_replace('/<\?xml version=\"1\.0\"\?><!DOCTYPE [^>]*>/', '', $xml);
+
+        // Hack to fix wrong mimetype.
+        $xml = str_replace(
+            array('application/vnd.syncml-devinf+wbxml',
+                  'xmlns="syncml:metinf1.0"',
+                  'xmlns="syncml:devinf1.0"',
+                  'xmlns="syncml:metinf1.1"',
+                  'xmlns="syncml:devinf1.1"'),
+            array('application/vnd.syncml-devinf+xml',
+                  'xmlns="syncml:metinf"',
+                  'xmlns="syncml:devinf"',
+                  'xmlns="syncml:metinf"',
+                  'xmlns="syncml:devinf"'),
+            $xml);
+    }
+
+    if (is_string($xml) && strcasecmp($xml, $xml_ref) === 0) {
+        echo "decode ok: $file\n";
+    } else {
+        echo "\ndecode FAILED: $file\nlibwbxml:  $xml_ref\n";
+        if (is_string($xml)) {
+            echo "XML_WBXML: $xml\n";
+        } elseif (is_a($xml, 'PEAR_Error')) {
+            echo "XML_WBXML: " . $xml->getMessage() . "\n";
+        } else {
+            echo "libwbxml:\n";
+            var_dump($xml);
+        }
+        echo "\n";
+    }
+}