From 89e08dcd286a07b4791ae59f84576750db724bbb Mon Sep 17 00:00:00 2001 From: Andyvanee <1andyvanee@gmail.com> Date: Mon, 22 Jul 2013 23:14:47 -0600 Subject: [PATCH] Setup --- Archive/Tar.php | 1815 +++++++ Auth.php | 1291 +++++ Auth/Anonymous.php | 138 + Auth/Auth.php | 30 + Auth/Container.php | 262 + Auth/Container/Array.php | 161 + Auth/Container/DB.php | 632 +++ Auth/Container/DBLite.php | 313 ++ Auth/Container/File.php | 314 ++ Auth/Container/IMAP.php | 210 + Auth/Container/KADM5.php | 171 + Auth/Container/LDAP.php | 766 +++ Auth/Container/MDB.php | 618 +++ Auth/Container/MDB2.php | 617 +++ Auth/Container/Multiple.php | 188 + Auth/Container/PEAR.php | 115 + Auth/Container/POP3.php | 145 + Auth/Container/RADIUS.php | 182 + Auth/Container/SAP.php | 179 + Auth/Container/SMBPasswd.php | 182 + Auth/Container/SOAP.php | 229 + Auth/Container/SOAP5.php | 268 + Auth/Container/vpopmail.php | 88 + Auth/Controller.php | 302 ++ Auth/Frontend/Html.php | 142 + Auth/Frontend/md5.js | 256 + Cache/Lite.php | 846 +++ Cache/Lite/File.php | 93 + Cache/Lite/Function.php | 211 + Cache/Lite/Output.php | 72 + Calendar/Calendar.php | 685 +++ Calendar/Day.php | 197 + Calendar/Decorator.php | 558 ++ Calendar/Decorator/Textual.php | 169 + Calendar/Decorator/Uri.php | 151 + Calendar/Decorator/Weekday.php | 148 + Calendar/Decorator/Wrapper.php | 90 + Calendar/Engine/Interface.php | 293 + Calendar/Engine/PearDate.php | 407 ++ Calendar/Engine/UnixTS.php | 365 ++ Calendar/Factory.php | 145 + Calendar/Hour.php | 113 + Calendar/Minute.php | 114 + Calendar/Month.php | 114 + Calendar/Month/Weekdays.php | 189 + Calendar/Month/Weeks.php | 139 + Calendar/Second.php | 98 + Calendar/Table/Helper.php | 280 + Calendar/Util/Textual.php | 239 + Calendar/Util/Uri.php | 169 + Calendar/Validator.php | 335 ++ Calendar/Week.php | 394 ++ Calendar/Year.php | 113 + Console/Getopt.php | 290 + Crypt/HMAC.php | 163 + Date.php | 1465 +++++ Date/Calc.php | 2117 ++++++++ Date/Human.php | 242 + Date/Span.php | 1083 ++++ Date/TimeZone.php | 4731 +++++++++++++++++ HTML/Common.php | 464 ++ HTML/QuickForm.php | 2054 +++++++ HTML/QuickForm/CAPTCHA.php | 237 + HTML/QuickForm/CAPTCHA/Equation.php | 95 + HTML/QuickForm/CAPTCHA/Figlet.php | 128 + HTML/QuickForm/CAPTCHA/Image.php | 154 + HTML/QuickForm/CAPTCHA/Word.php | 92 + HTML/QuickForm/Renderer.php | 158 + HTML/QuickForm/Renderer/Array.php | 340 ++ HTML/QuickForm/Renderer/ArraySmarty.php | 402 ++ HTML/QuickForm/Renderer/Default.php | 485 ++ HTML/QuickForm/Renderer/ITDynamic.php | 300 ++ HTML/QuickForm/Renderer/ITStatic.php | 504 ++ HTML/QuickForm/Renderer/Object.php | 461 ++ HTML/QuickForm/Renderer/ObjectFlexy.php | 291 + HTML/QuickForm/Renderer/QuickHtml.php | 213 + HTML/QuickForm/Rule.php | 82 + HTML/QuickForm/Rule/CAPTCHA.php | 47 + HTML/QuickForm/Rule/Callback.php | 124 + HTML/QuickForm/Rule/Compare.php | 105 + HTML/QuickForm/Rule/Email.php | 73 + HTML/QuickForm/Rule/Range.php | 75 + HTML/QuickForm/Rule/Regex.php | 101 + HTML/QuickForm/Rule/Required.php | 63 + HTML/QuickForm/RuleRegistry.php | 349 ++ HTML/QuickForm/advcheckbox.php | 286 + HTML/QuickForm/autocomplete.php | 258 + HTML/QuickForm/button.php | 80 + HTML/QuickForm/checkbox.php | 277 + HTML/QuickForm/date.php | 511 ++ HTML/QuickForm/element.php | 494 ++ HTML/QuickForm/file.php | 358 ++ HTML/QuickForm/group.php | 588 ++ HTML/QuickForm/header.php | 74 + HTML/QuickForm/hidden.php | 94 + HTML/QuickForm/hiddenselect.php | 118 + HTML/QuickForm/hierselect.php | 593 +++ HTML/QuickForm/html.php | 77 + HTML/QuickForm/image.php | 127 + HTML/QuickForm/input.php | 209 + HTML/QuickForm/link.php | 200 + HTML/QuickForm/password.php | 115 + HTML/QuickForm/radio.php | 251 + HTML/QuickForm/reset.php | 79 + HTML/QuickForm/select.php | 614 +++ HTML/QuickForm/static.php | 201 + HTML/QuickForm/submit.php | 89 + HTML/QuickForm/text.php | 98 + HTML/QuickForm/textarea.php | 229 + HTML/QuickForm/xbutton.php | 153 + HTTP.php | 359 ++ HTTP/Header.php | 531 ++ HTTP/Header/Cache.php | 238 + HTTP/Request.php | 1461 +++++ HTTP/Request/Listener.php | 106 + HTTP/Upload.php | 856 +++ Image/Text.php | 1261 +++++ Log.php | 824 +++ Log/composite.php | 231 + Log/console.php | 208 + Log/daemon.php | 230 + Log/display.php | 141 + Log/error_log.php | 127 + Log/file.php | 316 ++ Log/firebug.php | 210 + Log/mail.php | 257 + Log/mcal.php | 170 + Log/mdb2.php | 358 ++ Log/null.php | 91 + Log/observer.php | 129 + Log/sql.php | 294 + Log/sqlite.php | 225 + Log/syslog.php | 179 + Log/win.php | 269 + MDB2.php | 4271 +++++++++++++++ MDB2/Date.php | 183 + MDB2/Driver/Datatype/Common.php | 1837 +++++++ MDB2/Driver/Datatype/mysql.php | 471 ++ MDB2/Driver/Function/Common.php | 231 + MDB2/Driver/Function/mysql.php | 120 + MDB2/Driver/Manager/Common.php | 864 +++ MDB2/Driver/Manager/mysql.php | 1001 ++++ MDB2/Driver/Native/Common.php | 58 + MDB2/Driver/Native/mysql.php | 60 + MDB2/Driver/Reverse/Common.php | 476 ++ MDB2/Driver/Reverse/mysql.php | 437 ++ MDB2/Driver/mysql.php | 1479 ++++++ MDB2/Extended.php | 714 +++ MDB2/Iterator.php | 259 + MDB2/LOB.php | 264 + Mail.php | 238 + Mail/RFC822.php | 923 ++++ Mail/mail.php | 142 + Mail/mime.php | 1095 ++++ Mail/mimeDecode.php | 849 +++ Mail/mimePart.php | 439 ++ Mail/null.php | 60 + Mail/sendmail.php | 155 + Mail/smtp.php | 348 ++ Net/SMTP.php | 1034 ++++ Net/Socket.php | 576 ++ Net/URL.php | 485 ++ Numbers/Words.php | 188 + Numbers/Words/lang.bg.php | 505 ++ Numbers/Words/lang.cs.php | 339 ++ Numbers/Words/lang.de.php | 321 ++ Numbers/Words/lang.dk.php | 417 ++ Numbers/Words/lang.en_100.php | 307 ++ Numbers/Words/lang.en_GB.php | 424 ++ Numbers/Words/lang.en_US.php | 515 ++ Numbers/Words/lang.es.php | 343 ++ Numbers/Words/lang.es_AR.php | 474 ++ Numbers/Words/lang.et.php | 349 ++ Numbers/Words/lang.fr.php | 439 ++ Numbers/Words/lang.fr_BE.php | 417 ++ Numbers/Words/lang.he.php | 506 ++ Numbers/Words/lang.hu_HU.php | 402 ++ Numbers/Words/lang.id.php | 277 + Numbers/Words/lang.it_IT.php | 348 ++ Numbers/Words/lang.lt.php | 310 ++ Numbers/Words/lang.nl.php | 324 ++ Numbers/Words/lang.pl.php | 520 ++ Numbers/Words/lang.pt_BR.php | 331 ++ Numbers/Words/lang.ru.php | 624 +++ Numbers/Words/lang.sv.php | 310 ++ OLE.php | 410 ++ OLE/PPS.php | 219 + OLE/PPS/File.php | 114 + OLE/PPS/Root.php | 519 ++ OS/Guess.php | 344 ++ PEAR.php | 1108 ++++ PEAR/Autoloader.php | 223 + PEAR/Builder.php | 479 ++ PEAR/ChannelFile.php | 1615 ++++++ PEAR/ChannelFile/Parser.php | 73 + PEAR/Command.php | 416 ++ PEAR/Command/Auth.php | 203 + PEAR/Command/Auth.xml | 26 + PEAR/Command/Build.php | 104 + PEAR/Command/Build.xml | 10 + PEAR/Command/Channels.php | 737 +++ PEAR/Command/Channels.xml | 98 + PEAR/Command/Common.php | 291 + PEAR/Command/Config.php | 420 ++ PEAR/Command/Config.xml | 92 + PEAR/Command/Install.php | 1171 ++++ PEAR/Command/Install.xml | 259 + PEAR/Command/Mirror.php | 153 + PEAR/Command/Mirror.xml | 18 + PEAR/Command/Package.php | 844 +++ PEAR/Command/Package.xml | 194 + PEAR/Command/Pickle.php | 376 ++ PEAR/Command/Pickle.xml | 40 + PEAR/Command/Registry.php | 1066 ++++ PEAR/Command/Registry.xml | 58 + PEAR/Command/Remote.php | 812 +++ PEAR/Command/Remote.xml | 108 + PEAR/Command/Test.php | 330 ++ PEAR/Command/Test.xml | 54 + PEAR/Common.php | 1126 ++++ PEAR/Config.php | 2108 ++++++++ PEAR/Dependency.php | 495 ++ PEAR/Dependency2.php | 1299 +++++ PEAR/DependencyDB.php | 707 +++ PEAR/Downloader.php | 1746 ++++++ PEAR/Downloader/Package.php | 1851 +++++++ PEAR/ErrorStack.php | 985 ++++ PEAR/Exception.php | 397 ++ PEAR/Frontend.php | 223 + PEAR/Frontend/CLI.php | 794 +++ PEAR/Installer.php | 1678 ++++++ PEAR/Installer/Role.php | 253 + PEAR/Installer/Role/Common.php | 180 + PEAR/Installer/Role/Data.php | 34 + PEAR/Installer/Role/Data.xml | 15 + PEAR/Installer/Role/Doc.php | 34 + PEAR/Installer/Role/Doc.xml | 15 + PEAR/Installer/Role/Ext.php | 34 + PEAR/Installer/Role/Ext.xml | 12 + PEAR/Installer/Role/Php.php | 34 + PEAR/Installer/Role/Php.xml | 15 + PEAR/Installer/Role/Script.php | 34 + PEAR/Installer/Role/Script.xml | 15 + PEAR/Installer/Role/Src.php | 40 + PEAR/Installer/Role/Src.xml | 12 + PEAR/Installer/Role/Test.php | 34 + PEAR/Installer/Role/Test.xml | 15 + PEAR/PackageFile.php | 474 ++ PEAR/PackageFile/Generator/v1.php | 1272 +++++ PEAR/PackageFile/Generator/v2.php | 1529 ++++++ PEAR/PackageFile/Parser/v1.php | 461 ++ PEAR/PackageFile/Parser/v2.php | 117 + PEAR/PackageFile/v1.php | 1618 ++++++ PEAR/PackageFile/v2.php | 2042 +++++++ PEAR/PackageFile/v2/Validator.php | 2101 ++++++++ PEAR/PackageFile/v2/rw.php | 1603 ++++++ PEAR/Packager.php | 199 + PEAR/REST.php | 395 ++ PEAR/REST/10.php | 812 +++ PEAR/REST/11.php | 317 ++ PEAR/REST/13.php | 291 + PEAR/Registry.php | 2224 ++++++++ PEAR/Remote.php | 498 ++ PEAR/RunTest.php | 848 +++ PEAR/Task/Common.php | 208 + PEAR/Task/Postinstallscript.php | 329 ++ PEAR/Task/Postinstallscript/rw.php | 176 + PEAR/Task/Replace.php | 182 + PEAR/Task/Replace/rw.php | 67 + PEAR/Task/Unixeol.php | 83 + PEAR/Task/Unixeol/rw.php | 62 + PEAR/Task/Windowseol.php | 83 + PEAR/Task/Windowseol/rw.php | 62 + PEAR/Validate.php | 634 +++ PEAR/Validator/PECL.php | 63 + PEAR/XMLParser.php | 261 + Pager.php | 193 + Pager/Common.php | 1507 ++++++ Pager/HtmlWidgets.php | 229 + Pager/Jumping.php | 280 + Pager/Pager.php | 3 + Pager/Sliding.php | 324 ++ Pearified/Smarty/Config_File.class.php | 389 ++ Pearified/Smarty/Smarty.class.php | 1935 +++++++ Pearified/Smarty/Smarty_Compiler.class.php | 2301 ++++++++ Pearified/Smarty/debug.tpl | 64 + Pearified/Smarty/docs/examples/demo/index.php | 25 + .../core.assemble_plugin_filepath.php | 67 + .../core.assign_smarty_interface.php | 43 + .../internals/core.create_dir_structure.php | 79 + .../internals/core.display_debug_console.php | 61 + .../internals/core.get_include_path.php | 44 + .../Smarty/internals/core.get_microtime.php | 23 + .../internals/core.get_php_resource.php | 80 + Pearified/Smarty/internals/core.is_secure.php | 56 + .../Smarty/internals/core.is_trusted.php | 47 + .../Smarty/internals/core.load_plugins.php | 125 + .../internals/core.load_resource_plugin.php | 74 + .../internals/core.process_cached_inserts.php | 71 + .../core.process_compiled_include.php | 32 + .../Smarty/internals/core.read_cache_file.php | 111 + Pearified/Smarty/internals/core.rm_auto.php | 71 + Pearified/Smarty/internals/core.rmdir.php | 55 + .../internals/core.run_insert_handler.php | 71 + .../internals/core.smarty_include_php.php | 50 + .../internals/core.write_cache_file.php | 96 + .../internals/core.write_compiled_include.php | 91 + .../core.write_compiled_resource.php | 35 + .../Smarty/internals/core.write_file.php | 54 + Pearified/Smarty/plugins/block.textformat.php | 102 + Pearified/Smarty/plugins/compiler.assign.php | 38 + .../plugins/function.assign_debug_info.php | 39 + .../Smarty/plugins/function.config_load.php | 140 + Pearified/Smarty/plugins/function.counter.php | 79 + Pearified/Smarty/plugins/function.cycle.php | 102 + Pearified/Smarty/plugins/function.debug.php | 35 + Pearified/Smarty/plugins/function.eval.php | 48 + Pearified/Smarty/plugins/function.fetch.php | 220 + .../plugins/function.html_checkboxes.php | 143 + .../Smarty/plugins/function.html_image.php | 139 + .../Smarty/plugins/function.html_options.php | 121 + .../Smarty/plugins/function.html_radios.php | 150 + .../plugins/function.html_select_date.php | 316 ++ .../plugins/function.html_select_time.php | 192 + .../Smarty/plugins/function.html_table.php | 137 + Pearified/Smarty/plugins/function.mailto.php | 163 + Pearified/Smarty/plugins/function.math.php | 82 + Pearified/Smarty/plugins/function.popup.php | 117 + .../Smarty/plugins/function.popup_init.php | 39 + .../Smarty/plugins/modifier.capitalize.php | 42 + Pearified/Smarty/plugins/modifier.cat.php | 33 + .../plugins/modifier.count_characters.php | 31 + .../plugins/modifier.count_paragraphs.php | 28 + .../plugins/modifier.count_sentences.php | 28 + .../Smarty/plugins/modifier.count_words.php | 32 + .../Smarty/plugins/modifier.date_format.php | 48 + .../plugins/modifier.debug_print_var.php | 56 + Pearified/Smarty/plugins/modifier.default.php | 31 + Pearified/Smarty/plugins/modifier.escape.php | 89 + Pearified/Smarty/plugins/modifier.indent.php | 27 + Pearified/Smarty/plugins/modifier.lower.php | 25 + Pearified/Smarty/plugins/modifier.nl2br.php | 35 + .../Smarty/plugins/modifier.regex_replace.php | 33 + Pearified/Smarty/plugins/modifier.replace.php | 29 + Pearified/Smarty/plugins/modifier.spacify.php | 29 + .../Smarty/plugins/modifier.string_format.php | 28 + Pearified/Smarty/plugins/modifier.strip.php | 33 + .../Smarty/plugins/modifier.strip_tags.php | 31 + .../Smarty/plugins/modifier.truncate.php | 43 + Pearified/Smarty/plugins/modifier.upper.php | 25 + .../Smarty/plugins/modifier.wordwrap.php | 28 + .../plugins/outputfilter.trimwhitespace.php | 75 + .../plugins/shared.escape_special_chars.php | 30 + .../Smarty/plugins/shared.make_timestamp.php | 43 + Pearified/Smarty/tests/config.php | 5 + Pearified/Smarty/tests/smarty_unit_test.php | 10 + .../Smarty/tests/smarty_unit_test_gui.php | 10 + Pearified/Smarty/tests/test_cases.php | 450 ++ Spreadsheet/Excel/Writer.php | 104 + Spreadsheet/Excel/Writer/BIFFwriter.php | 238 + Spreadsheet/Excel/Writer/Format.php | 1102 ++++ Spreadsheet/Excel/Writer/Parser.php | 1689 ++++++ Spreadsheet/Excel/Writer/Validator.php | 230 + Spreadsheet/Excel/Writer/Workbook.php | 1528 ++++++ Spreadsheet/Excel/Writer/Worksheet.php | 3502 ++++++++++++ Structures/Graph.php | 154 + Structures/Graph/Manipulator/AcyclicTest.php | 136 + .../Graph/Manipulator/TopologicalSorter.php | 153 + Structures/Graph/Node.php | 338 ++ System.php | 594 +++ Text/CAPTCHA.php | 253 + Text/CAPTCHA/Driver/Equation.php | 224 + Text/CAPTCHA/Driver/Figlet.php | 263 + Text/CAPTCHA/Driver/Image.php | 362 ++ Text/CAPTCHA/Driver/Numeral.php | 436 ++ Text/CAPTCHA/Driver/Word.php | 120 + Text/Figlet.php | 451 ++ Text/Password.php | 536 ++ XML/Parser.php | 690 +++ XML/Parser/Simple.php | 297 ++ XML/RSS.php | 359 ++ XML/Tree.php | 370 ++ XML/Tree/Node.php | 354 ++ composer.json | 5 + pearcmd.php | 446 ++ peclcmd.php | 45 + tests/TextTest.php | 270 + tests/imageisthesame.php | 77 + tests/imageisthesameTest.php | 132 + tests/testimages/10x5-red.png | Bin 0 -> 153 bytes tests/testimages/10x5-white-grey.png | Bin 0 -> 152 bytes tests/testimages/10x5-white-index.png | Bin 0 -> 162 bytes tests/testimages/10x5-white.png | Bin 0 -> 157 bytes tests/testimages/5x10-gradient.jpg | Bin 0 -> 435 bytes tests/testimages/5x10-gradient.png | Bin 0 -> 241 bytes tests/testimages/5x10-red-254.jpg | Bin 0 -> 336 bytes tests/testimages/5x10-red-254.png | Bin 0 -> 153 bytes tests/testimages/5x10-red-index.png | Bin 0 -> 162 bytes tests/testimages/5x10-red.png | Bin 0 -> 176 bytes tests/testimages/test-background-red.png | Bin 0 -> 773 bytes .../test-background-transparent.png | Bin 0 -> 734 bytes tests/testimages/test-construct.png | Bin 0 -> 543 bytes 402 files changed, 151265 insertions(+) create mode 100644 Archive/Tar.php create mode 100644 Auth.php create mode 100644 Auth/Anonymous.php create mode 100644 Auth/Auth.php create mode 100644 Auth/Container.php create mode 100644 Auth/Container/Array.php create mode 100644 Auth/Container/DB.php create mode 100644 Auth/Container/DBLite.php create mode 100644 Auth/Container/File.php create mode 100644 Auth/Container/IMAP.php create mode 100644 Auth/Container/KADM5.php create mode 100644 Auth/Container/LDAP.php create mode 100644 Auth/Container/MDB.php create mode 100644 Auth/Container/MDB2.php create mode 100644 Auth/Container/Multiple.php create mode 100644 Auth/Container/PEAR.php create mode 100644 Auth/Container/POP3.php create mode 100644 Auth/Container/RADIUS.php create mode 100644 Auth/Container/SAP.php create mode 100644 Auth/Container/SMBPasswd.php create mode 100644 Auth/Container/SOAP.php create mode 100644 Auth/Container/SOAP5.php create mode 100644 Auth/Container/vpopmail.php create mode 100644 Auth/Controller.php create mode 100644 Auth/Frontend/Html.php create mode 100644 Auth/Frontend/md5.js create mode 100644 Cache/Lite.php create mode 100644 Cache/Lite/File.php create mode 100644 Cache/Lite/Function.php create mode 100644 Cache/Lite/Output.php create mode 100644 Calendar/Calendar.php create mode 100644 Calendar/Day.php create mode 100644 Calendar/Decorator.php create mode 100644 Calendar/Decorator/Textual.php create mode 100644 Calendar/Decorator/Uri.php create mode 100644 Calendar/Decorator/Weekday.php create mode 100644 Calendar/Decorator/Wrapper.php create mode 100644 Calendar/Engine/Interface.php create mode 100644 Calendar/Engine/PearDate.php create mode 100644 Calendar/Engine/UnixTS.php create mode 100644 Calendar/Factory.php create mode 100644 Calendar/Hour.php create mode 100644 Calendar/Minute.php create mode 100644 Calendar/Month.php create mode 100644 Calendar/Month/Weekdays.php create mode 100644 Calendar/Month/Weeks.php create mode 100644 Calendar/Second.php create mode 100644 Calendar/Table/Helper.php create mode 100644 Calendar/Util/Textual.php create mode 100644 Calendar/Util/Uri.php create mode 100644 Calendar/Validator.php create mode 100644 Calendar/Week.php create mode 100644 Calendar/Year.php create mode 100644 Console/Getopt.php create mode 100644 Crypt/HMAC.php create mode 100644 Date.php create mode 100644 Date/Calc.php create mode 100644 Date/Human.php create mode 100644 Date/Span.php create mode 100644 Date/TimeZone.php create mode 100644 HTML/Common.php create mode 100644 HTML/QuickForm.php create mode 100644 HTML/QuickForm/CAPTCHA.php create mode 100644 HTML/QuickForm/CAPTCHA/Equation.php create mode 100644 HTML/QuickForm/CAPTCHA/Figlet.php create mode 100644 HTML/QuickForm/CAPTCHA/Image.php create mode 100644 HTML/QuickForm/CAPTCHA/Word.php create mode 100644 HTML/QuickForm/Renderer.php create mode 100644 HTML/QuickForm/Renderer/Array.php create mode 100644 HTML/QuickForm/Renderer/ArraySmarty.php create mode 100644 HTML/QuickForm/Renderer/Default.php create mode 100644 HTML/QuickForm/Renderer/ITDynamic.php create mode 100644 HTML/QuickForm/Renderer/ITStatic.php create mode 100644 HTML/QuickForm/Renderer/Object.php create mode 100644 HTML/QuickForm/Renderer/ObjectFlexy.php create mode 100644 HTML/QuickForm/Renderer/QuickHtml.php create mode 100644 HTML/QuickForm/Rule.php create mode 100644 HTML/QuickForm/Rule/CAPTCHA.php create mode 100644 HTML/QuickForm/Rule/Callback.php create mode 100644 HTML/QuickForm/Rule/Compare.php create mode 100644 HTML/QuickForm/Rule/Email.php create mode 100644 HTML/QuickForm/Rule/Range.php create mode 100644 HTML/QuickForm/Rule/Regex.php create mode 100644 HTML/QuickForm/Rule/Required.php create mode 100644 HTML/QuickForm/RuleRegistry.php create mode 100644 HTML/QuickForm/advcheckbox.php create mode 100644 HTML/QuickForm/autocomplete.php create mode 100644 HTML/QuickForm/button.php create mode 100644 HTML/QuickForm/checkbox.php create mode 100644 HTML/QuickForm/date.php create mode 100644 HTML/QuickForm/element.php create mode 100644 HTML/QuickForm/file.php create mode 100644 HTML/QuickForm/group.php create mode 100644 HTML/QuickForm/header.php create mode 100644 HTML/QuickForm/hidden.php create mode 100644 HTML/QuickForm/hiddenselect.php create mode 100644 HTML/QuickForm/hierselect.php create mode 100644 HTML/QuickForm/html.php create mode 100644 HTML/QuickForm/image.php create mode 100644 HTML/QuickForm/input.php create mode 100644 HTML/QuickForm/link.php create mode 100644 HTML/QuickForm/password.php create mode 100644 HTML/QuickForm/radio.php create mode 100644 HTML/QuickForm/reset.php create mode 100644 HTML/QuickForm/select.php create mode 100644 HTML/QuickForm/static.php create mode 100644 HTML/QuickForm/submit.php create mode 100644 HTML/QuickForm/text.php create mode 100644 HTML/QuickForm/textarea.php create mode 100644 HTML/QuickForm/xbutton.php create mode 100644 HTTP.php create mode 100644 HTTP/Header.php create mode 100644 HTTP/Header/Cache.php create mode 100644 HTTP/Request.php create mode 100644 HTTP/Request/Listener.php create mode 100644 HTTP/Upload.php create mode 100644 Image/Text.php create mode 100644 Log.php create mode 100644 Log/composite.php create mode 100644 Log/console.php create mode 100644 Log/daemon.php create mode 100644 Log/display.php create mode 100644 Log/error_log.php create mode 100644 Log/file.php create mode 100644 Log/firebug.php create mode 100644 Log/mail.php create mode 100644 Log/mcal.php create mode 100644 Log/mdb2.php create mode 100644 Log/null.php create mode 100644 Log/observer.php create mode 100644 Log/sql.php create mode 100644 Log/sqlite.php create mode 100644 Log/syslog.php create mode 100644 Log/win.php create mode 100644 MDB2.php create mode 100644 MDB2/Date.php create mode 100644 MDB2/Driver/Datatype/Common.php create mode 100644 MDB2/Driver/Datatype/mysql.php create mode 100644 MDB2/Driver/Function/Common.php create mode 100644 MDB2/Driver/Function/mysql.php create mode 100644 MDB2/Driver/Manager/Common.php create mode 100644 MDB2/Driver/Manager/mysql.php create mode 100644 MDB2/Driver/Native/Common.php create mode 100644 MDB2/Driver/Native/mysql.php create mode 100644 MDB2/Driver/Reverse/Common.php create mode 100644 MDB2/Driver/Reverse/mysql.php create mode 100644 MDB2/Driver/mysql.php create mode 100644 MDB2/Extended.php create mode 100644 MDB2/Iterator.php create mode 100644 MDB2/LOB.php create mode 100644 Mail.php create mode 100644 Mail/RFC822.php create mode 100644 Mail/mail.php create mode 100644 Mail/mime.php create mode 100644 Mail/mimeDecode.php create mode 100644 Mail/mimePart.php create mode 100644 Mail/null.php create mode 100644 Mail/sendmail.php create mode 100644 Mail/smtp.php create mode 100644 Net/SMTP.php create mode 100644 Net/Socket.php create mode 100644 Net/URL.php create mode 100644 Numbers/Words.php create mode 100644 Numbers/Words/lang.bg.php create mode 100644 Numbers/Words/lang.cs.php create mode 100644 Numbers/Words/lang.de.php create mode 100644 Numbers/Words/lang.dk.php create mode 100644 Numbers/Words/lang.en_100.php create mode 100644 Numbers/Words/lang.en_GB.php create mode 100644 Numbers/Words/lang.en_US.php create mode 100644 Numbers/Words/lang.es.php create mode 100644 Numbers/Words/lang.es_AR.php create mode 100644 Numbers/Words/lang.et.php create mode 100644 Numbers/Words/lang.fr.php create mode 100644 Numbers/Words/lang.fr_BE.php create mode 100644 Numbers/Words/lang.he.php create mode 100644 Numbers/Words/lang.hu_HU.php create mode 100644 Numbers/Words/lang.id.php create mode 100644 Numbers/Words/lang.it_IT.php create mode 100644 Numbers/Words/lang.lt.php create mode 100644 Numbers/Words/lang.nl.php create mode 100644 Numbers/Words/lang.pl.php create mode 100644 Numbers/Words/lang.pt_BR.php create mode 100644 Numbers/Words/lang.ru.php create mode 100644 Numbers/Words/lang.sv.php create mode 100644 OLE.php create mode 100644 OLE/PPS.php create mode 100644 OLE/PPS/File.php create mode 100644 OLE/PPS/Root.php create mode 100644 OS/Guess.php create mode 100644 PEAR.php create mode 100644 PEAR/Autoloader.php create mode 100644 PEAR/Builder.php create mode 100644 PEAR/ChannelFile.php create mode 100644 PEAR/ChannelFile/Parser.php create mode 100644 PEAR/Command.php create mode 100644 PEAR/Command/Auth.php create mode 100644 PEAR/Command/Auth.xml create mode 100644 PEAR/Command/Build.php create mode 100644 PEAR/Command/Build.xml create mode 100644 PEAR/Command/Channels.php create mode 100644 PEAR/Command/Channels.xml create mode 100644 PEAR/Command/Common.php create mode 100644 PEAR/Command/Config.php create mode 100644 PEAR/Command/Config.xml create mode 100644 PEAR/Command/Install.php create mode 100644 PEAR/Command/Install.xml create mode 100644 PEAR/Command/Mirror.php create mode 100644 PEAR/Command/Mirror.xml create mode 100644 PEAR/Command/Package.php create mode 100644 PEAR/Command/Package.xml create mode 100644 PEAR/Command/Pickle.php create mode 100644 PEAR/Command/Pickle.xml create mode 100644 PEAR/Command/Registry.php create mode 100644 PEAR/Command/Registry.xml create mode 100644 PEAR/Command/Remote.php create mode 100644 PEAR/Command/Remote.xml create mode 100644 PEAR/Command/Test.php create mode 100644 PEAR/Command/Test.xml create mode 100644 PEAR/Common.php create mode 100644 PEAR/Config.php create mode 100644 PEAR/Dependency.php create mode 100644 PEAR/Dependency2.php create mode 100644 PEAR/DependencyDB.php create mode 100644 PEAR/Downloader.php create mode 100644 PEAR/Downloader/Package.php create mode 100644 PEAR/ErrorStack.php create mode 100644 PEAR/Exception.php create mode 100644 PEAR/Frontend.php create mode 100644 PEAR/Frontend/CLI.php create mode 100644 PEAR/Installer.php create mode 100644 PEAR/Installer/Role.php create mode 100644 PEAR/Installer/Role/Common.php create mode 100644 PEAR/Installer/Role/Data.php create mode 100644 PEAR/Installer/Role/Data.xml create mode 100644 PEAR/Installer/Role/Doc.php create mode 100644 PEAR/Installer/Role/Doc.xml create mode 100644 PEAR/Installer/Role/Ext.php create mode 100644 PEAR/Installer/Role/Ext.xml create mode 100644 PEAR/Installer/Role/Php.php create mode 100644 PEAR/Installer/Role/Php.xml create mode 100644 PEAR/Installer/Role/Script.php create mode 100644 PEAR/Installer/Role/Script.xml create mode 100644 PEAR/Installer/Role/Src.php create mode 100644 PEAR/Installer/Role/Src.xml create mode 100644 PEAR/Installer/Role/Test.php create mode 100644 PEAR/Installer/Role/Test.xml create mode 100644 PEAR/PackageFile.php create mode 100644 PEAR/PackageFile/Generator/v1.php create mode 100644 PEAR/PackageFile/Generator/v2.php create mode 100644 PEAR/PackageFile/Parser/v1.php create mode 100644 PEAR/PackageFile/Parser/v2.php create mode 100644 PEAR/PackageFile/v1.php create mode 100644 PEAR/PackageFile/v2.php create mode 100644 PEAR/PackageFile/v2/Validator.php create mode 100644 PEAR/PackageFile/v2/rw.php create mode 100644 PEAR/Packager.php create mode 100644 PEAR/REST.php create mode 100644 PEAR/REST/10.php create mode 100644 PEAR/REST/11.php create mode 100644 PEAR/REST/13.php create mode 100644 PEAR/Registry.php create mode 100644 PEAR/Remote.php create mode 100644 PEAR/RunTest.php create mode 100644 PEAR/Task/Common.php create mode 100644 PEAR/Task/Postinstallscript.php create mode 100644 PEAR/Task/Postinstallscript/rw.php create mode 100644 PEAR/Task/Replace.php create mode 100644 PEAR/Task/Replace/rw.php create mode 100644 PEAR/Task/Unixeol.php create mode 100644 PEAR/Task/Unixeol/rw.php create mode 100644 PEAR/Task/Windowseol.php create mode 100644 PEAR/Task/Windowseol/rw.php create mode 100644 PEAR/Validate.php create mode 100644 PEAR/Validator/PECL.php create mode 100644 PEAR/XMLParser.php create mode 100644 Pager.php create mode 100644 Pager/Common.php create mode 100644 Pager/HtmlWidgets.php create mode 100644 Pager/Jumping.php create mode 100644 Pager/Pager.php create mode 100644 Pager/Sliding.php create mode 100644 Pearified/Smarty/Config_File.class.php create mode 100644 Pearified/Smarty/Smarty.class.php create mode 100644 Pearified/Smarty/Smarty_Compiler.class.php create mode 100644 Pearified/Smarty/debug.tpl create mode 100644 Pearified/Smarty/docs/examples/demo/index.php create mode 100644 Pearified/Smarty/internals/core.assemble_plugin_filepath.php create mode 100644 Pearified/Smarty/internals/core.assign_smarty_interface.php create mode 100644 Pearified/Smarty/internals/core.create_dir_structure.php create mode 100644 Pearified/Smarty/internals/core.display_debug_console.php create mode 100644 Pearified/Smarty/internals/core.get_include_path.php create mode 100644 Pearified/Smarty/internals/core.get_microtime.php create mode 100644 Pearified/Smarty/internals/core.get_php_resource.php create mode 100644 Pearified/Smarty/internals/core.is_secure.php create mode 100644 Pearified/Smarty/internals/core.is_trusted.php create mode 100644 Pearified/Smarty/internals/core.load_plugins.php create mode 100644 Pearified/Smarty/internals/core.load_resource_plugin.php create mode 100644 Pearified/Smarty/internals/core.process_cached_inserts.php create mode 100644 Pearified/Smarty/internals/core.process_compiled_include.php create mode 100644 Pearified/Smarty/internals/core.read_cache_file.php create mode 100644 Pearified/Smarty/internals/core.rm_auto.php create mode 100644 Pearified/Smarty/internals/core.rmdir.php create mode 100644 Pearified/Smarty/internals/core.run_insert_handler.php create mode 100644 Pearified/Smarty/internals/core.smarty_include_php.php create mode 100644 Pearified/Smarty/internals/core.write_cache_file.php create mode 100644 Pearified/Smarty/internals/core.write_compiled_include.php create mode 100644 Pearified/Smarty/internals/core.write_compiled_resource.php create mode 100644 Pearified/Smarty/internals/core.write_file.php create mode 100644 Pearified/Smarty/plugins/block.textformat.php create mode 100644 Pearified/Smarty/plugins/compiler.assign.php create mode 100644 Pearified/Smarty/plugins/function.assign_debug_info.php create mode 100644 Pearified/Smarty/plugins/function.config_load.php create mode 100644 Pearified/Smarty/plugins/function.counter.php create mode 100644 Pearified/Smarty/plugins/function.cycle.php create mode 100644 Pearified/Smarty/plugins/function.debug.php create mode 100644 Pearified/Smarty/plugins/function.eval.php create mode 100644 Pearified/Smarty/plugins/function.fetch.php create mode 100644 Pearified/Smarty/plugins/function.html_checkboxes.php create mode 100644 Pearified/Smarty/plugins/function.html_image.php create mode 100644 Pearified/Smarty/plugins/function.html_options.php create mode 100644 Pearified/Smarty/plugins/function.html_radios.php create mode 100644 Pearified/Smarty/plugins/function.html_select_date.php create mode 100644 Pearified/Smarty/plugins/function.html_select_time.php create mode 100644 Pearified/Smarty/plugins/function.html_table.php create mode 100644 Pearified/Smarty/plugins/function.mailto.php create mode 100644 Pearified/Smarty/plugins/function.math.php create mode 100644 Pearified/Smarty/plugins/function.popup.php create mode 100644 Pearified/Smarty/plugins/function.popup_init.php create mode 100644 Pearified/Smarty/plugins/modifier.capitalize.php create mode 100644 Pearified/Smarty/plugins/modifier.cat.php create mode 100644 Pearified/Smarty/plugins/modifier.count_characters.php create mode 100644 Pearified/Smarty/plugins/modifier.count_paragraphs.php create mode 100644 Pearified/Smarty/plugins/modifier.count_sentences.php create mode 100644 Pearified/Smarty/plugins/modifier.count_words.php create mode 100644 Pearified/Smarty/plugins/modifier.date_format.php create mode 100644 Pearified/Smarty/plugins/modifier.debug_print_var.php create mode 100644 Pearified/Smarty/plugins/modifier.default.php create mode 100644 Pearified/Smarty/plugins/modifier.escape.php create mode 100644 Pearified/Smarty/plugins/modifier.indent.php create mode 100644 Pearified/Smarty/plugins/modifier.lower.php create mode 100644 Pearified/Smarty/plugins/modifier.nl2br.php create mode 100644 Pearified/Smarty/plugins/modifier.regex_replace.php create mode 100644 Pearified/Smarty/plugins/modifier.replace.php create mode 100644 Pearified/Smarty/plugins/modifier.spacify.php create mode 100644 Pearified/Smarty/plugins/modifier.string_format.php create mode 100644 Pearified/Smarty/plugins/modifier.strip.php create mode 100644 Pearified/Smarty/plugins/modifier.strip_tags.php create mode 100644 Pearified/Smarty/plugins/modifier.truncate.php create mode 100644 Pearified/Smarty/plugins/modifier.upper.php create mode 100644 Pearified/Smarty/plugins/modifier.wordwrap.php create mode 100644 Pearified/Smarty/plugins/outputfilter.trimwhitespace.php create mode 100644 Pearified/Smarty/plugins/shared.escape_special_chars.php create mode 100644 Pearified/Smarty/plugins/shared.make_timestamp.php create mode 100644 Pearified/Smarty/tests/config.php create mode 100644 Pearified/Smarty/tests/smarty_unit_test.php create mode 100644 Pearified/Smarty/tests/smarty_unit_test_gui.php create mode 100644 Pearified/Smarty/tests/test_cases.php create mode 100644 Spreadsheet/Excel/Writer.php create mode 100644 Spreadsheet/Excel/Writer/BIFFwriter.php create mode 100644 Spreadsheet/Excel/Writer/Format.php create mode 100644 Spreadsheet/Excel/Writer/Parser.php create mode 100644 Spreadsheet/Excel/Writer/Validator.php create mode 100644 Spreadsheet/Excel/Writer/Workbook.php create mode 100644 Spreadsheet/Excel/Writer/Worksheet.php create mode 100644 Structures/Graph.php create mode 100644 Structures/Graph/Manipulator/AcyclicTest.php create mode 100644 Structures/Graph/Manipulator/TopologicalSorter.php create mode 100644 Structures/Graph/Node.php create mode 100644 System.php create mode 100644 Text/CAPTCHA.php create mode 100644 Text/CAPTCHA/Driver/Equation.php create mode 100644 Text/CAPTCHA/Driver/Figlet.php create mode 100644 Text/CAPTCHA/Driver/Image.php create mode 100644 Text/CAPTCHA/Driver/Numeral.php create mode 100644 Text/CAPTCHA/Driver/Word.php create mode 100644 Text/Figlet.php create mode 100644 Text/Password.php create mode 100644 XML/Parser.php create mode 100644 XML/Parser/Simple.php create mode 100644 XML/RSS.php create mode 100644 XML/Tree.php create mode 100644 XML/Tree/Node.php create mode 100644 composer.json create mode 100644 pearcmd.php create mode 100644 peclcmd.php create mode 100644 tests/TextTest.php create mode 100644 tests/imageisthesame.php create mode 100644 tests/imageisthesameTest.php create mode 100644 tests/testimages/10x5-red.png create mode 100644 tests/testimages/10x5-white-grey.png create mode 100644 tests/testimages/10x5-white-index.png create mode 100644 tests/testimages/10x5-white.png create mode 100644 tests/testimages/5x10-gradient.jpg create mode 100644 tests/testimages/5x10-gradient.png create mode 100644 tests/testimages/5x10-red-254.jpg create mode 100644 tests/testimages/5x10-red-254.png create mode 100644 tests/testimages/5x10-red-index.png create mode 100644 tests/testimages/5x10-red.png create mode 100644 tests/testimages/test-background-red.png create mode 100644 tests/testimages/test-background-transparent.png create mode 100644 tests/testimages/test-construct.png diff --git a/Archive/Tar.php b/Archive/Tar.php new file mode 100644 index 0000000..b14e488 --- /dev/null +++ b/Archive/Tar.php @@ -0,0 +1,1815 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Tar.php,v 1.39 2006/12/22 19:20:08 cellog Exp $ + +require_once 'PEAR.php'; + + +define ('ARCHIVE_TAR_ATT_SEPARATOR', 90001); +define ('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + +/** +* Creates a (compressed) Tar archive +* +* @author Vincent Blavet +* @version $Revision: 1.39 $ +* @package Archive +*/ +class Archive_Tar extends PEAR +{ + /** + * @var string Name of the Tar + */ + var $_tarname=''; + + /** + * @var boolean if true, the Tar file will be gzipped + */ + var $_compress=false; + + /** + * @var string Type of compression : 'none', 'gz' or 'bz2' + */ + var $_compress_type='none'; + + /** + * @var string Explode separator + */ + var $_separator=' '; + + /** + * @var file descriptor + */ + var $_file=0; + + /** + * @var string Local Tar name of a remote Tar (http:// or ftp://) + */ + var $_temp_tarname=''; + + // {{{ constructor + /** + * Archive_Tar Class constructor. This flavour of the constructor only + * declare a new Archive_Tar object, identifying it by the name of the + * tar file. + * If the compress argument is set the tar will be read or created as a + * gzip or bz2 compressed TAR file. + * + * @param string $p_tarname The name of the tar archive to create + * @param string $p_compress can be null, 'gz' or 'bz2'. This + * parameter indicates if gzip or bz2 compression + * is required. For compatibility reason the + * boolean value 'true' means 'gz'. + * @access public + */ + function Archive_Tar($p_tarname, $p_compress = null) + { + $this->PEAR(); + $this->_compress = false; + $this->_compress_type = 'none'; + if (($p_compress === null) || ($p_compress == '')) { + if (@file_exists($p_tarname)) { + if ($fp = @fopen($p_tarname, "rb")) { + // look for gzip magic cookie + $data = fread($fp, 2); + fclose($fp); + if ($data == "\37\213") { + $this->_compress = true; + $this->_compress_type = 'gz'; + // No sure it's enought for a magic code .... + } elseif ($data == "BZ") { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } + } + } else { + // probably a remote file or some file accessible + // through a stream interface + if (substr($p_tarname, -2) == 'gz') { + $this->_compress = true; + $this->_compress_type = 'gz'; + } elseif ((substr($p_tarname, -3) == 'bz2') || + (substr($p_tarname, -2) == 'bz')) { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } + } + } else { + if (($p_compress === true) || ($p_compress == 'gz')) { + $this->_compress = true; + $this->_compress_type = 'gz'; + } else if ($p_compress == 'bz2') { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } else { + die("Unsupported compression type '$p_compress'\n". + "Supported types are 'gz' and 'bz2'.\n"); + return false; + } + } + $this->_tarname = $p_tarname; + if ($this->_compress) { // assert zlib or bz2 extension support + if ($this->_compress_type == 'gz') + $extname = 'zlib'; + else if ($this->_compress_type == 'bz2') + $extname = 'bz2'; + + if (!extension_loaded($extname)) { + PEAR::loadExtension($extname); + } + if (!extension_loaded($extname)) { + die("The extension '$extname' couldn't be found.\n". + "Please make sure your version of PHP was built ". + "with '$extname' support.\n"); + return false; + } + } + } + // }}} + + // {{{ destructor + function _Archive_Tar() + { + $this->_close(); + // ----- Look for a local copy to delete + if ($this->_temp_tarname != '') + @unlink($this->_temp_tarname); + $this->_PEAR(); + } + // }}} + + // {{{ create() + /** + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If a file with the same name exist and is writable, it is replaced + * by the new tar. + * The method return false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * For each directory added in the archive, the files and + * sub-directories are also added. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * @return true on success, false on error. + * @see createModify() + * @access public + */ + function create($p_filelist) + { + return $this->createModify($p_filelist, '', ''); + } + // }}} + + // {{{ add() + /** + * This method add the files / directories that are listed in $p_filelist in + * the archive. If the archive does not exist it is created. + * The method return false and a PEAR error text. + * The files and directories listed are only added at the end of the archive, + * even if a file with the same name is already archived. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * @return true on success, false on error. + * @see createModify() + * @access public + */ + function add($p_filelist) + { + return $this->addModify($p_filelist, '', ''); + } + // }}} + + // {{{ extract() + function extract($p_path='') + { + return $this->extractModify($p_path, ''); + } + // }}} + + // {{{ listContent() + function listContent() + { + $v_list_detail = array(); + + if ($this->_openRead()) { + if (!$this->_extractList('', $v_list_detail, "list", '', '')) { + unset($v_list_detail); + $v_list_detail = 0; + } + $this->_close(); + } + + return $v_list_detail; + } + // }}} + + // {{{ createModify() + /** + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If the file already exists and is writable, it is replaced by the + * new tar. It is a create and not an add. If the file exists and is + * read-only or is a directory it is not replaced. The method return + * false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * See also addModify() method for file adding properties. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated by + * a single blank space. + * @param string $p_add_dir A string which contains a path to be added + * to the memorized path of each element in + * the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of each + * element in the list, when relevant. + * @return boolean true on success, false on error. + * @access public + * @see addModify() + */ + function createModify($p_filelist, $p_add_dir, $p_remove_dir='') + { + $v_result = true; + + if (!$this->_openWrite()) + return false; + + if ($p_filelist != '') { + if (is_array($p_filelist)) + $v_list = $p_filelist; + elseif (is_string($p_filelist)) + $v_list = explode($this->_separator, $p_filelist); + else { + $this->_cleanFile(); + $this->_error('Invalid file list'); + return false; + } + + $v_result = $this->_addList($v_list, $p_add_dir, $p_remove_dir); + } + + if ($v_result) { + $this->_writeFooter(); + $this->_close(); + } else + $this->_cleanFile(); + + return $v_result; + } + // }}} + + // {{{ addModify() + /** + * This method add the files / directories listed in $p_filelist at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * If a file/dir is already in the archive it will only be added at the + * end of the archive. There is no update of the existing archived + * file/dir. However while extracting the archive, the last file will + * replace the first one. This results in a none optimization of the + * archive size. + * If a file/dir does not exist the file/dir is ignored. However an + * error text is send to PEAR error. + * If a file/dir is not readable the file/dir is ignored. However an + * error text is send to PEAR error. + * + * @param array $p_filelist An array of filenames and directory + * names, or a single string with names + * separated by a single blank space. + * @param string $p_add_dir A string which contains a path to be + * added to the memorized path of each + * element in the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of + * each element in the list, when + * relevant. + * @return true on success, false on error. + * @access public + */ + function addModify($p_filelist, $p_add_dir, $p_remove_dir='') + { + $v_result = true; + + if (!$this->_isArchive()) + $v_result = $this->createModify($p_filelist, $p_add_dir, + $p_remove_dir); + else { + if (is_array($p_filelist)) + $v_list = $p_filelist; + elseif (is_string($p_filelist)) + $v_list = explode($this->_separator, $p_filelist); + else { + $this->_error('Invalid file list'); + return false; + } + + $v_result = $this->_append($v_list, $p_add_dir, $p_remove_dir); + } + + return $v_result; + } + // }}} + + // {{{ addString() + /** + * This method add a single string as a file at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * + * @param string $p_filename A string which contains the full + * filename path that will be associated + * with the string. + * @param string $p_string The content of the file added in + * the archive. + * @return true on success, false on error. + * @access public + */ + function addString($p_filename, $p_string) + { + $v_result = true; + + if (!$this->_isArchive()) { + if (!$this->_openWrite()) { + return false; + } + $this->_close(); + } + + if (!$this->_openAppend()) + return false; + + // Need to check the get back to the temporary file ? .... + $v_result = $this->_addString($p_filename, $p_string); + + $this->_writeFooter(); + + $this->_close(); + + return $v_result; + } + // }}} + + // {{{ extractModify() + /** + * This method extract all the content of the archive in the directory + * indicated by $p_path. When relevant the memorized path of the + * files/dir can be modified by removing the $p_remove_path path at the + * beginning of the file/dir path. + * While extracting a file, if the directory path does not exists it is + * created. + * While extracting a file, if the file already exists it is replaced + * without looking for last modification date. + * While extracting a file, if the file already exists and is write + * protected, the extraction is aborted. + * While extracting a file, if a directory with the same name already + * exists, the extraction is aborted. + * While extracting a directory, if a file with the same name already + * exists, the extraction is aborted. + * While extracting a file/directory if the destination directory exist + * and is write protected, or does not exist but can not be created, + * the extraction is aborted. + * If after extraction an extracted file does not show the correct + * stored file size, the extraction is aborted. + * When the extraction is aborted, a PEAR error text is set and false + * is returned. However the result can be a partial extraction that may + * need to be manually cleaned. + * + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @return boolean true on success, false on error. + * @access public + * @see extractList() + */ + function extractModify($p_path, $p_remove_path) + { + $v_result = true; + $v_list_detail = array(); + + if ($v_result = $this->_openRead()) { + $v_result = $this->_extractList($p_path, $v_list_detail, + "complete", 0, $p_remove_path); + $this->_close(); + } + + return $v_result; + } + // }}} + + // {{{ extractInString() + /** + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or NULL on error. + * @param string $p_filename The path of the file to extract in a string. + * @return a string with the file content or NULL. + * @access public + */ + function extractInString($p_filename) + { + if ($this->_openRead()) { + $v_result = $this->_extractInString($p_filename); + $this->_close(); + } else { + $v_result = NULL; + } + + return $v_result; + } + // }}} + + // {{{ extractList() + /** + * This method extract from the archive only the files indicated in the + * $p_filelist. These files are extracted in the current directory or + * in the directory indicated by the optional $p_path parameter. + * If indicated the $p_remove_path can be used in the same way as it is + * used in extractModify() method. + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated + * by a single blank space. + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @return true on success, false on error. + * @access public + * @see extractModify() + */ + function extractList($p_filelist, $p_path='', $p_remove_path='') + { + $v_result = true; + $v_list_detail = array(); + + if (is_array($p_filelist)) + $v_list = $p_filelist; + elseif (is_string($p_filelist)) + $v_list = explode($this->_separator, $p_filelist); + else { + $this->_error('Invalid string list'); + return false; + } + + if ($v_result = $this->_openRead()) { + $v_result = $this->_extractList($p_path, $v_list_detail, "partial", + $v_list, $p_remove_path); + $this->_close(); + } + + return $v_result; + } + // }}} + + // {{{ setAttribute() + /** + * This method set specific attributes of the archive. It uses a variable + * list of parameters, in the format attribute code + attribute values : + * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); + * @param mixed $argv variable list of attributes and values + * @return true on success, false on error. + * @access public + */ + function setAttribute() + { + $v_result = true; + + // ----- Get the number of variable list of arguments + if (($v_size = func_num_args()) == 0) { + return true; + } + + // ----- Get the arguments + $v_att_list = &func_get_args(); + + // ----- Read the attributes + $i=0; + while ($i<$v_size) { + + // ----- Look for next option + switch ($v_att_list[$i]) { + // ----- Look for options that request a string value + case ARCHIVE_TAR_ATT_SEPARATOR : + // ----- Check the number of parameters + if (($i+1) >= $v_size) { + $this->_error('Invalid number of parameters for ' + .'attribute ARCHIVE_TAR_ATT_SEPARATOR'); + return false; + } + + // ----- Get the value + $this->_separator = $v_att_list[$i+1]; + $i++; + break; + + default : + $this->_error('Unknow attribute code '.$v_att_list[$i].''); + return false; + } + + // ----- Next attribute + $i++; + } + + return $v_result; + } + // }}} + + // {{{ _error() + function _error($p_message) + { + // ----- To be completed + $this->raiseError($p_message); + } + // }}} + + // {{{ _warning() + function _warning($p_message) + { + // ----- To be completed + $this->raiseError($p_message); + } + // }}} + + // {{{ _isArchive() + function _isArchive($p_filename=NULL) + { + if ($p_filename == NULL) { + $p_filename = $this->_tarname; + } + clearstatcache(); + return @is_file($p_filename); + } + // }}} + + // {{{ _openWrite() + function _openWrite() + { + if ($this->_compress_type == 'gz') + $this->_file = @gzopen($this->_tarname, "wb9"); + else if ($this->_compress_type == 'bz2') + $this->_file = @bzopen($this->_tarname, "wb"); + else if ($this->_compress_type == 'none') + $this->_file = @fopen($this->_tarname, "wb"); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + if ($this->_file == 0) { + $this->_error('Unable to open in write mode \'' + .$this->_tarname.'\''); + return false; + } + + return true; + } + // }}} + + // {{{ _openRead() + function _openRead() + { + if (strtolower(substr($this->_tarname, 0, 7)) == 'http://') { + + // ----- Look if a local copy need to be done + if ($this->_temp_tarname == '') { + $this->_temp_tarname = uniqid('tar').'.tmp'; + if (!$v_file_from = @fopen($this->_tarname, 'rb')) { + $this->_error('Unable to open in read mode \'' + .$this->_tarname.'\''); + $this->_temp_tarname = ''; + return false; + } + if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { + $this->_error('Unable to open in write mode \'' + .$this->_temp_tarname.'\''); + $this->_temp_tarname = ''; + return false; + } + while ($v_data = @fread($v_file_from, 1024)) + @fwrite($v_file_to, $v_data); + @fclose($v_file_from); + @fclose($v_file_to); + } + + // ----- File to open if the local copy + $v_filename = $this->_temp_tarname; + + } else + // ----- File to open if the normal Tar file + $v_filename = $this->_tarname; + + if ($this->_compress_type == 'gz') + $this->_file = @gzopen($v_filename, "rb"); + else if ($this->_compress_type == 'bz2') + $this->_file = @bzopen($v_filename, "rb"); + else if ($this->_compress_type == 'none') + $this->_file = @fopen($v_filename, "rb"); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + if ($this->_file == 0) { + $this->_error('Unable to open in read mode \''.$v_filename.'\''); + return false; + } + + return true; + } + // }}} + + // {{{ _openReadWrite() + function _openReadWrite() + { + if ($this->_compress_type == 'gz') + $this->_file = @gzopen($this->_tarname, "r+b"); + else if ($this->_compress_type == 'bz2') + $this->_file = @bzopen($this->_tarname, "r+b"); + else if ($this->_compress_type == 'none') + $this->_file = @fopen($this->_tarname, "r+b"); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + if ($this->_file == 0) { + $this->_error('Unable to open in read/write mode \'' + .$this->_tarname.'\''); + return false; + } + + return true; + } + // }}} + + // {{{ _close() + function _close() + { + //if (isset($this->_file)) { + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') + @gzclose($this->_file); + else if ($this->_compress_type == 'bz2') + @bzclose($this->_file); + else if ($this->_compress_type == 'none') + @fclose($this->_file); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + $this->_file = 0; + } + + // ----- Look if a local copy need to be erase + // Note that it might be interesting to keep the url for a time : ToDo + if ($this->_temp_tarname != '') { + @unlink($this->_temp_tarname); + $this->_temp_tarname = ''; + } + + return true; + } + // }}} + + // {{{ _cleanFile() + function _cleanFile() + { + $this->_close(); + + // ----- Look for a local copy + if ($this->_temp_tarname != '') { + // ----- Remove the local copy but not the remote tarname + @unlink($this->_temp_tarname); + $this->_temp_tarname = ''; + } else { + // ----- Remove the local tarname file + @unlink($this->_tarname); + } + $this->_tarname = ''; + + return true; + } + // }}} + + // {{{ _writeBlock() + function _writeBlock($p_binary_data, $p_len=null) + { + if (is_resource($this->_file)) { + if ($p_len === null) { + if ($this->_compress_type == 'gz') + @gzputs($this->_file, $p_binary_data); + else if ($this->_compress_type == 'bz2') + @bzwrite($this->_file, $p_binary_data); + else if ($this->_compress_type == 'none') + @fputs($this->_file, $p_binary_data); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'gz') + @gzputs($this->_file, $p_binary_data, $p_len); + else if ($this->_compress_type == 'bz2') + @bzwrite($this->_file, $p_binary_data, $p_len); + else if ($this->_compress_type == 'none') + @fputs($this->_file, $p_binary_data, $p_len); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + } + } + return true; + } + // }}} + + // {{{ _readBlock() + function _readBlock() + { + $v_block = null; + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') + $v_block = @gzread($this->_file, 512); + else if ($this->_compress_type == 'bz2') + $v_block = @bzread($this->_file, 512); + else if ($this->_compress_type == 'none') + $v_block = @fread($this->_file, 512); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + } + return $v_block; + } + // }}} + + // {{{ _jumpBlock() + function _jumpBlock($p_len=null) + { + if (is_resource($this->_file)) { + if ($p_len === null) + $p_len = 1; + + if ($this->_compress_type == 'gz') { + @gzseek($this->_file, gztell($this->_file)+($p_len*512)); + } + else if ($this->_compress_type == 'bz2') { + // ----- Replace missing bztell() and bzseek() + for ($i=0; $i<$p_len; $i++) + $this->_readBlock(); + } else if ($this->_compress_type == 'none') + @fseek($this->_file, ftell($this->_file)+($p_len*512)); + else + $this->_error('Unknown or missing compression type (' + .$this->_compress_type.')'); + + } + return true; + } + // }}} + + // {{{ _writeFooter() + function _writeFooter() + { + if (is_resource($this->_file)) { + // ----- Write the last 0 filled block for end of archive + $v_binary_data = pack('a1024', ''); + $this->_writeBlock($v_binary_data); + } + return true; + } + // }}} + + // {{{ _addList() + function _addList($p_list, $p_add_dir, $p_remove_dir) + { + $v_result=true; + $v_header = array(); + + // ----- Remove potential windows directory separator + $p_add_dir = $this->_translateWinPath($p_add_dir); + $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); + + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if (sizeof($p_list) == 0) + return true; + + foreach ($p_list as $v_filename) { + if (!$v_result) { + break; + } + + // ----- Skip the current tar name + if ($v_filename == $this->_tarname) + continue; + + if ($v_filename == '') + continue; + + if (!file_exists($v_filename)) { + $this->_warning("File '$v_filename' does not exist"); + continue; + } + + // ----- Add the file or directory header + if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) + return false; + + if (@is_dir($v_filename)) { + if (!($p_hdir = opendir($v_filename))) { + $this->_warning("Directory '$v_filename' can not be read"); + continue; + } + while (false !== ($p_hitem = readdir($p_hdir))) { + if (($p_hitem != '.') && ($p_hitem != '..')) { + if ($v_filename != ".") + $p_temp_list[0] = $v_filename.'/'.$p_hitem; + else + $p_temp_list[0] = $p_hitem; + + $v_result = $this->_addList($p_temp_list, + $p_add_dir, + $p_remove_dir); + } + } + + unset($p_temp_list); + unset($p_hdir); + unset($p_hitem); + } + } + + return $v_result; + } + // }}} + + // {{{ _addFile() + function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir) + { + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false);; + $v_stored_filename = $p_filename; + if (strcmp($p_filename, $p_remove_dir) == 0) { + return true; + } + if ($p_remove_dir != '') { + if (substr($p_remove_dir, -1) != '/') + $p_remove_dir .= '/'; + + if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) + $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); + } + $v_stored_filename = $this->_translateWinPath($v_stored_filename); + if ($p_add_dir != '') { + if (substr($p_add_dir, -1) == '/') + $v_stored_filename = $p_add_dir.$v_stored_filename; + else + $v_stored_filename = $p_add_dir.'/'.$v_stored_filename; + } + + $v_stored_filename = $this->_pathReduction($v_stored_filename); + + if ($this->_isArchive($p_filename)) { + if (($v_file = @fopen($p_filename, "rb")) == 0) { + $this->_warning("Unable to open file '".$p_filename + ."' in binary read mode"); + return true; + } + + if (!$this->_writeHeader($p_filename, $v_stored_filename)) + return false; + + while (($v_buffer = fread($v_file, 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + fclose($v_file); + + } else { + // ----- Only header for dir + if (!$this->_writeHeader($p_filename, $v_stored_filename)) + return false; + } + + return true; + } + // }}} + + // {{{ _addString() + function _addString($p_filename, $p_string) + { + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false);; + + if (!$this->_writeHeaderBlock($p_filename, strlen($p_string), + time(), 384, "", 0, 0)) + return false; + + $i=0; + while (($v_buffer = substr($p_string, (($i++)*512), 512)) != '') { + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + return true; + } + // }}} + + // {{{ _writeHeader() + function _writeHeader($p_filename, $p_stored_filename) + { + if ($p_stored_filename == '') + $p_stored_filename = $p_filename; + $v_reduce_filename = $this->_pathReduction($p_stored_filename); + + if (strlen($v_reduce_filename) > 99) { + if (!$this->_writeLongHeader($v_reduce_filename)) + return false; + } + + $v_info = stat($p_filename); + $v_uid = sprintf("%6s ", DecOct($v_info[4])); + $v_gid = sprintf("%6s ", DecOct($v_info[5])); + $v_perms = sprintf("%6s ", DecOct(fileperms($p_filename))); + + $v_mtime = sprintf("%11s", DecOct(filemtime($p_filename))); + + if (@is_dir($p_filename)) { + $v_typeflag = "5"; + $v_size = sprintf("%11s ", DecOct(0)); + } else { + $v_typeflag = ''; + clearstatcache(); + $v_size = sprintf("%11s ", DecOct(filesize($p_filename))); + } + + $v_linkname = ''; + + $v_magic = ''; + + $v_version = ''; + + $v_uname = ''; + + $v_gname = ''; + + $v_devmajor = ''; + + $v_devminor = ''; + + $v_prefix = ''; + + $v_binary_data_first = pack("a100a8a8a8a12A12", + $v_reduce_filename, $v_perms, $v_uid, + $v_gid, $v_size, $v_mtime); + $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, $v_linkname, $v_magic, + $v_version, $v_uname, $v_gname, + $v_devmajor, $v_devminor, $v_prefix, ''); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i=0; $i<148; $i++) + $v_checksum += ord(substr($v_binary_data_first,$i,1)); + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i=148; $i<156; $i++) + $v_checksum += ord(' '); + // ..... Last part of the header + for ($i=156, $j=0; $i<512; $i++, $j++) + $v_checksum += ord(substr($v_binary_data_last,$j,1)); + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + return true; + } + // }}} + + // {{{ _writeHeaderBlock() + function _writeHeaderBlock($p_filename, $p_size, $p_mtime=0, $p_perms=0, + $p_type='', $p_uid=0, $p_gid=0) + { + $p_filename = $this->_pathReduction($p_filename); + + if (strlen($p_filename) > 99) { + if (!$this->_writeLongHeader($p_filename)) + return false; + } + + if ($p_type == "5") { + $v_size = sprintf("%11s ", DecOct(0)); + } else { + $v_size = sprintf("%11s ", DecOct($p_size)); + } + + $v_uid = sprintf("%6s ", DecOct($p_uid)); + $v_gid = sprintf("%6s ", DecOct($p_gid)); + $v_perms = sprintf("%6s ", DecOct($p_perms)); + + $v_mtime = sprintf("%11s", DecOct($p_mtime)); + + $v_linkname = ''; + + $v_magic = ''; + + $v_version = ''; + + $v_uname = ''; + + $v_gname = ''; + + $v_devmajor = ''; + + $v_devminor = ''; + + $v_prefix = ''; + + $v_binary_data_first = pack("a100a8a8a8a12A12", + $p_filename, $v_perms, $v_uid, $v_gid, + $v_size, $v_mtime); + $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", + $p_type, $v_linkname, $v_magic, + $v_version, $v_uname, $v_gname, + $v_devmajor, $v_devminor, $v_prefix, ''); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i=0; $i<148; $i++) + $v_checksum += ord(substr($v_binary_data_first,$i,1)); + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i=148; $i<156; $i++) + $v_checksum += ord(' '); + // ..... Last part of the header + for ($i=156, $j=0; $i<512; $i++, $j++) + $v_checksum += ord(substr($v_binary_data_last,$j,1)); + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + return true; + } + // }}} + + // {{{ _writeLongHeader() + function _writeLongHeader($p_filename) + { + $v_size = sprintf("%11s ", DecOct(strlen($p_filename))); + + $v_typeflag = 'L'; + + $v_linkname = ''; + + $v_magic = ''; + + $v_version = ''; + + $v_uname = ''; + + $v_gname = ''; + + $v_devmajor = ''; + + $v_devminor = ''; + + $v_prefix = ''; + + $v_binary_data_first = pack("a100a8a8a8a12A12", + '././@LongLink', 0, 0, 0, $v_size, 0); + $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, $v_linkname, $v_magic, + $v_version, $v_uname, $v_gname, + $v_devmajor, $v_devminor, $v_prefix, ''); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i=0; $i<148; $i++) + $v_checksum += ord(substr($v_binary_data_first,$i,1)); + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i=148; $i<156; $i++) + $v_checksum += ord(' '); + // ..... Last part of the header + for ($i=156, $j=0; $i<512; $i++, $j++) + $v_checksum += ord(substr($v_binary_data_last,$j,1)); + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + // ----- Write the filename as content of the block + $i=0; + while (($v_buffer = substr($p_filename, (($i++)*512), 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + return true; + } + // }}} + + // {{{ _readHeader() + function _readHeader($v_binary_data, &$v_header) + { + if (strlen($v_binary_data)==0) { + $v_header['filename'] = ''; + return true; + } + + if (strlen($v_binary_data) != 512) { + $v_header['filename'] = ''; + $this->_error('Invalid block size : '.strlen($v_binary_data)); + return false; + } + + if (!is_array($v_header)) { + $v_header = array(); + } + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i=0; $i<148; $i++) + $v_checksum+=ord(substr($v_binary_data,$i,1)); + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i=148; $i<156; $i++) + $v_checksum += ord(' '); + // ..... Last part of the header + for ($i=156; $i<512; $i++) + $v_checksum+=ord(substr($v_binary_data,$i,1)); + + $v_data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" + ."a8checksum/a1typeflag/a100link/a6magic/a2version/" + ."a32uname/a32gname/a8devmajor/a8devminor", + $v_binary_data); + + // ----- Extract the checksum + $v_header['checksum'] = OctDec(trim($v_data['checksum'])); + if ($v_header['checksum'] != $v_checksum) { + $v_header['filename'] = ''; + + // ----- Look for last block (empty block) + if (($v_checksum == 256) && ($v_header['checksum'] == 0)) + return true; + + $this->_error('Invalid checksum for file "'.$v_data['filename'] + .'" : '.$v_checksum.' calculated, ' + .$v_header['checksum'].' expected'); + return false; + } + + // ----- Extract the properties + $v_header['filename'] = trim($v_data['filename']); + if ($this->_maliciousFilename($v_header['filename'])) { + $this->_error('Malicious .tar detected, file "' . $v_header['filename'] . + '" will not install in desired directory tree'); + return false; + } + $v_header['mode'] = OctDec(trim($v_data['mode'])); + $v_header['uid'] = OctDec(trim($v_data['uid'])); + $v_header['gid'] = OctDec(trim($v_data['gid'])); + $v_header['size'] = OctDec(trim($v_data['size'])); + $v_header['mtime'] = OctDec(trim($v_data['mtime'])); + if (($v_header['typeflag'] = $v_data['typeflag']) == "5") { + $v_header['size'] = 0; + } + $v_header['link'] = trim($v_data['link']); + /* ----- All these fields are removed form the header because + they do not carry interesting info + $v_header[magic] = trim($v_data[magic]); + $v_header[version] = trim($v_data[version]); + $v_header[uname] = trim($v_data[uname]); + $v_header[gname] = trim($v_data[gname]); + $v_header[devmajor] = trim($v_data[devmajor]); + $v_header[devminor] = trim($v_data[devminor]); + */ + + return true; + } + // }}} + + // {{{ _maliciousFilename() + /** + * Detect and report a malicious file name + * + * @param string $file + * @return bool + * @access private + */ + function _maliciousFilename($file) + { + if (strpos($file, '/../') !== false) { + return true; + } + if (strpos($file, '../') === 0) { + return true; + } + return false; + } + // }}} + + // {{{ _readLongHeader() + function _readLongHeader(&$v_header) + { + $v_filename = ''; + $n = floor($v_header['size']/512); + for ($i=0; $i<$n; $i++) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + + // ----- Read the next header + $v_binary_data = $this->_readBlock(); + + if (!$this->_readHeader($v_binary_data, $v_header)) + return false; + + $v_header['filename'] = $v_filename; + if ($this->_maliciousFilename($v_filename)) { + $this->_error('Malicious .tar detected, file "' . $v_filename . + '" will not install in desired directory tree'); + return false; + } + + return true; + } + // }}} + + // {{{ _extractInString() + /** + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or NULL on error. + * @param string $p_filename The path of the file to extract in a string. + * @return a string with the file content or NULL. + * @access private + */ + function _extractInString($p_filename) + { + $v_result_str = ""; + + While (strlen($v_binary_data = $this->_readBlock()) != 0) + { + if (!$this->_readHeader($v_binary_data, $v_header)) + return NULL; + + if ($v_header['filename'] == '') + continue; + + // ----- Look for long filename + if ($v_header['typeflag'] == 'L') { + if (!$this->_readLongHeader($v_header)) + return NULL; + } + + if ($v_header['filename'] == $p_filename) { + if ($v_header['typeflag'] == "5") { + $this->_error('Unable to extract in string a directory ' + .'entry {'.$v_header['filename'].'}'); + return NULL; + } else { + $n = floor($v_header['size']/512); + for ($i=0; $i<$n; $i++) { + $v_result_str .= $this->_readBlock(); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_result_str .= substr($v_content, 0, + ($v_header['size'] % 512)); + } + return $v_result_str; + } + } else { + $this->_jumpBlock(ceil(($v_header['size']/512))); + } + } + + return NULL; + } + // }}} + + // {{{ _extractList() + function _extractList($p_path, &$p_list_detail, $p_mode, + $p_file_list, $p_remove_path) + { + $v_result=true; + $v_nb = 0; + $v_extract_all = true; + $v_listing = false; + + $p_path = $this->_translateWinPath($p_path, false); + if ($p_path == '' || (substr($p_path, 0, 1) != '/' + && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':'))) { + $p_path = "./".$p_path; + } + $p_remove_path = $this->_translateWinPath($p_remove_path); + + // ----- Look for path to remove format (should end by /) + if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) + $p_remove_path .= '/'; + $p_remove_path_size = strlen($p_remove_path); + + switch ($p_mode) { + case "complete" : + $v_extract_all = TRUE; + $v_listing = FALSE; + break; + case "partial" : + $v_extract_all = FALSE; + $v_listing = FALSE; + break; + case "list" : + $v_extract_all = FALSE; + $v_listing = TRUE; + break; + default : + $this->_error('Invalid extract mode ('.$p_mode.')'); + return false; + } + + clearstatcache(); + + while (strlen($v_binary_data = $this->_readBlock()) != 0) + { + $v_extract_file = FALSE; + $v_extraction_stopped = 0; + + if (!$this->_readHeader($v_binary_data, $v_header)) + return false; + + if ($v_header['filename'] == '') { + continue; + } + + // ----- Look for long filename + if ($v_header['typeflag'] == 'L') { + if (!$this->_readLongHeader($v_header)) + return false; + } + + if ((!$v_extract_all) && (is_array($p_file_list))) { + // ----- By default no unzip if the file is not found + $v_extract_file = false; + + for ($i=0; $i strlen($p_file_list[$i])) + && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) + == $p_file_list[$i])) { + $v_extract_file = TRUE; + break; + } + } + + // ----- It is a file, so compare the file names + elseif ($p_file_list[$i] == $v_header['filename']) { + $v_extract_file = TRUE; + break; + } + } + } else { + $v_extract_file = TRUE; + } + + // ----- Look if this file need to be extracted + if (($v_extract_file) && (!$v_listing)) + { + if (($p_remove_path != '') + && (substr($v_header['filename'], 0, $p_remove_path_size) + == $p_remove_path)) + $v_header['filename'] = substr($v_header['filename'], + $p_remove_path_size); + if (($p_path != './') && ($p_path != '/')) { + while (substr($p_path, -1) == '/') + $p_path = substr($p_path, 0, strlen($p_path)-1); + + if (substr($v_header['filename'], 0, 1) == '/') + $v_header['filename'] = $p_path.$v_header['filename']; + else + $v_header['filename'] = $p_path.'/'.$v_header['filename']; + } + if (file_exists($v_header['filename'])) { + if ( (@is_dir($v_header['filename'])) + && ($v_header['typeflag'] == '')) { + $this->_error('File '.$v_header['filename'] + .' already exists as a directory'); + return false; + } + if ( ($this->_isArchive($v_header['filename'])) + && ($v_header['typeflag'] == "5")) { + $this->_error('Directory '.$v_header['filename'] + .' already exists as a file'); + return false; + } + if (!is_writeable($v_header['filename'])) { + $this->_error('File '.$v_header['filename'] + .' already exists and is write protected'); + return false; + } + if (filemtime($v_header['filename']) > $v_header['mtime']) { + // To be completed : An error or silent no replace ? + } + } + + // ----- Check the directory availability and create it if necessary + elseif (($v_result + = $this->_dirCheck(($v_header['typeflag'] == "5" + ?$v_header['filename'] + :dirname($v_header['filename'])))) != 1) { + $this->_error('Unable to create path for '.$v_header['filename']); + return false; + } + + if ($v_extract_file) { + if ($v_header['typeflag'] == "5") { + if (!@file_exists($v_header['filename'])) { + if (!@mkdir($v_header['filename'], 0777)) { + $this->_error('Unable to create directory {' + .$v_header['filename'].'}'); + return false; + } + } + } elseif ($v_header['typeflag'] == "2") { + if (!@symlink($v_header['link'], $v_header['filename'])) { + $this->_error('Unable to extract symbolic link {' + .$v_header['filename'].'}'); + return false; + } + } else { + if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { + $this->_error('Error while opening {'.$v_header['filename'] + .'} in write binary mode'); + return false; + } else { + $n = floor($v_header['size']/512); + for ($i=0; $i<$n; $i++) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, 512); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); + } + + @fclose($v_dest_file); + + // ----- Change the file mode, mtime + @touch($v_header['filename'], $v_header['mtime']); + if ($v_header['mode'] & 0111) { + // make file executable, obey umask + $mode = fileperms($v_header['filename']) | (~umask() & 0111); + @chmod($v_header['filename'], $mode); + } + } + + // ----- Check the file size + clearstatcache(); + if (filesize($v_header['filename']) != $v_header['size']) { + $this->_error('Extracted file '.$v_header['filename'] + .' does not have the correct file size \'' + .filesize($v_header['filename']) + .'\' ('.$v_header['size'] + .' expected). Archive may be corrupted.'); + return false; + } + } + } else { + $this->_jumpBlock(ceil(($v_header['size']/512))); + } + } else { + $this->_jumpBlock(ceil(($v_header['size']/512))); + } + + /* TBC : Seems to be unused ... + if ($this->_compress) + $v_end_of_file = @gzeof($this->_file); + else + $v_end_of_file = @feof($this->_file); + */ + + if ($v_listing || $v_extract_file || $v_extraction_stopped) { + // ----- Log extracted files + if (($v_file_dir = dirname($v_header['filename'])) + == $v_header['filename']) + $v_file_dir = ''; + if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) + $v_file_dir = '/'; + + $p_list_detail[$v_nb++] = $v_header; + if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { + return true; + } + } + } + + return true; + } + // }}} + + // {{{ _openAppend() + function _openAppend() + { + if (filesize($this->_tarname) == 0) + return $this->_openWrite(); + + if ($this->_compress) { + $this->_close(); + + if (!@rename($this->_tarname, $this->_tarname.".tmp")) { + $this->_error('Error while renaming \''.$this->_tarname + .'\' to temporary file \''.$this->_tarname + .'.tmp\''); + return false; + } + + if ($this->_compress_type == 'gz') + $v_temp_tar = @gzopen($this->_tarname.".tmp", "rb"); + elseif ($this->_compress_type == 'bz2') + $v_temp_tar = @bzopen($this->_tarname.".tmp", "rb"); + + if ($v_temp_tar == 0) { + $this->_error('Unable to open file \''.$this->_tarname + .'.tmp\' in binary read mode'); + @rename($this->_tarname.".tmp", $this->_tarname); + return false; + } + + if (!$this->_openWrite()) { + @rename($this->_tarname.".tmp", $this->_tarname); + return false; + } + + if ($this->_compress_type == 'gz') { + while (!@gzeof($v_temp_tar)) { + $v_buffer = @gzread($v_temp_tar, 512); + if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + // do not copy end blocks, we will re-make them + // after appending + continue; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @gzclose($v_temp_tar); + } + elseif ($this->_compress_type == 'bz2') { + while (strlen($v_buffer = @bzread($v_temp_tar, 512)) > 0) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + continue; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @bzclose($v_temp_tar); + } + + if (!@unlink($this->_tarname.".tmp")) { + $this->_error('Error while deleting temporary file \'' + .$this->_tarname.'.tmp\''); + } + + } else { + // ----- For not compressed tar, just add files before the last + // one or two 512 bytes block + if (!$this->_openReadWrite()) + return false; + + clearstatcache(); + $v_size = filesize($this->_tarname); + + // We might have zero, one or two end blocks. + // The standard is two, but we should try to handle + // other cases. + fseek($this->_file, $v_size - 1024); + if (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + fseek($this->_file, $v_size - 1024); + } + elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + fseek($this->_file, $v_size - 512); + } + } + + return true; + } + // }}} + + // {{{ _append() + function _append($p_filelist, $p_add_dir='', $p_remove_dir='') + { + if (!$this->_openAppend()) + return false; + + if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) + $this->_writeFooter(); + + $this->_close(); + + return true; + } + // }}} + + // {{{ _dirCheck() + + /** + * Check if a directory exists and create it (including parent + * dirs) if not. + * + * @param string $p_dir directory to check + * + * @return bool TRUE if the directory exists or was created + */ + function _dirCheck($p_dir) + { + clearstatcache(); + if ((@is_dir($p_dir)) || ($p_dir == '')) + return true; + + $p_parent_dir = dirname($p_dir); + + if (($p_parent_dir != $p_dir) && + ($p_parent_dir != '') && + (!$this->_dirCheck($p_parent_dir))) + return false; + + if (!@mkdir($p_dir, 0777)) { + $this->_error("Unable to create directory '$p_dir'"); + return false; + } + + return true; + } + + // }}} + + // {{{ _pathReduction() + + /** + * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar", + * rand emove double slashes. + * + * @param string $p_dir path to reduce + * + * @return string reduced path + * + * @access private + * + */ + function _pathReduction($p_dir) + { + $v_result = ''; + + // ----- Look for not empty path + if ($p_dir != '') { + // ----- Explode path by directory names + $v_list = explode('/', $p_dir); + + // ----- Study directories from last to first + for ($i=sizeof($v_list)-1; $i>=0; $i--) { + // ----- Look for current path + if ($v_list[$i] == ".") { + // ----- Ignore this directory + // Should be the first $i=0, but no check is done + } + else if ($v_list[$i] == "..") { + // ----- Ignore it and ignore the $i-1 + $i--; + } + else if ( ($v_list[$i] == '') + && ($i!=(sizeof($v_list)-1)) + && ($i!=0)) { + // ----- Ignore only the double '//' in path, + // but not the first and last / + } else { + $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?'/' + .$v_result:''); + } + } + } + $v_result = strtr($v_result, '\\', '/'); + return $v_result; + } + + // }}} + + // {{{ _translateWinPath() + function _translateWinPath($p_path, $p_remove_disk_letter=true) + { + if (defined('OS_WINDOWS') && OS_WINDOWS) { + // ----- Look for potential disk letter + if ( ($p_remove_disk_letter) + && (($v_position = strpos($p_path, ':')) != false)) { + $p_path = substr($p_path, $v_position+1); + } + // ----- Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) { + $p_path = strtr($p_path, '\\', '/'); + } + } + return $p_path; + } + // }}} + +} +?> diff --git a/Auth.php b/Auth.php new file mode 100644 index 0000000..c5a5a07 --- /dev/null +++ b/Auth.php @@ -0,0 +1,1291 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Auth.php,v 1.119 2007/07/02 03:38:52 aashley Exp $ + * @link http://pear.php.net/package/Auth + */ + +/** + * Returned if session exceeds idle time + */ +define('AUTH_IDLED', -1); +/** + * Returned if session has expired + */ +define('AUTH_EXPIRED', -2); +/** + * Returned if container is unable to authenticate user/password pair + */ +define('AUTH_WRONG_LOGIN', -3); +/** + * Returned if a container method is not supported. + */ +define('AUTH_METHOD_NOT_SUPPORTED', -4); +/** + * Returned if new Advanced security system detects a breach + */ +define('AUTH_SECURITY_BREACH', -5); +/** + * Returned if checkAuthCallback says session should not continue. + */ +define('AUTH_CALLBACK_ABORT', -6); + +/** + * Auth Log level - INFO + */ +define('AUTH_LOG_INFO', 6); +/** + * Auth Log level - DEBUG + */ +define('AUTH_LOG_DEBUG', 7); + + +/** + * PEAR::Auth + * + * The PEAR::Auth class provides methods for creating an + * authentication system using PHP. + * + * @category Authentication + * @package Auth + * @author Martin Jansen + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.119 $ + * @link http://pear.php.net/package/Auth + */ +class Auth { + + // {{{ properties + + /** + * Auth lifetime in seconds + * + * If this variable is set to 0, auth never expires + * + * @var integer + * @see setExpire(), checkAuth() + */ + var $expire = 0; + + /** + * Has the auth session expired? + * + * @var bool + * @see checkAuth() + */ + var $expired = false; + + /** + * Maximum idletime in seconds + * + * The difference to $expire is, that the idletime gets + * refreshed each time checkAuth() is called. If this + * variable is set to 0, idletime is never checked. + * + * @var integer + * @see setIdle(), checkAuth() + */ + var $idle = 0; + + /** + * Is the maximum idletime over? + * + * @var boolean + * @see checkAuth() + */ + var $idled = false; + + /** + * Storage object + * + * @var object + * @see Auth(), validateLogin() + */ + var $storage = ''; + + /** + * User-defined function that creates the login screen + * + * @var string + */ + var $loginFunction = ''; + + /** + * Should the login form be displayed + * + * @var bool + * @see setShowlogin() + */ + var $showLogin = true; + + /** + * Is Login Allowed from this page + * + * @var bool + * @see setAllowLogin + */ + var $allowLogin = true; + + /** + * Current authentication status + * + * @var string + */ + var $status = ''; + + /** + * Username + * + * @var string + */ + var $username = ''; + + /** + * Password + * + * @var string + */ + var $password = ''; + + /** + * checkAuth callback function name + * + * @var string + * @see setCheckAuthCallback() + */ + var $checkAuthCallback = ''; + + /** + * Login callback function name + * + * @var string + * @see setLoginCallback() + */ + var $loginCallback = ''; + + /** + * Failed Login callback function name + * + * @var string + * @see setFailedLoginCallback() + */ + var $loginFailedCallback = ''; + + /** + * Logout callback function name + * + * @var string + * @see setLogoutCallback() + */ + var $logoutCallback = ''; + + /** + * Auth session-array name + * + * @var string + */ + var $_sessionName = '_authsession'; + + /** + * Package Version + * + * @var string + */ + var $version = "@version@"; + + /** + * Flag to use advanced security + * When set extra checks will be made to see if the + * user's IP or useragent have changed across requests. + * Turned off by default to preserve BC. + * + * @var boolean + */ + var $advancedsecurity = false; + + /** + * Username key in POST array + * + * @var string + */ + var $_postUsername = 'username'; + + /** + * Password key in POST array + * + * @var string + */ + var $_postPassword = 'password'; + + /** + * Holds a reference to the session auth variable + * @var array + */ + var $session; + + /** + * Holds a reference to the global server variable + * @var array + */ + var $server; + + /** + * Holds a reference to the global post variable + * @var array + */ + var $post; + + /** + * Holds a reference to the global cookie variable + * @var array + */ + var $cookie; + + /** + * A hash to hold various superglobals as reference + * @var array + */ + var $authdata; + + /** + * How many times has checkAuth been called + * @var int + */ + var $authChecks = 0; + + /** + * PEAR::Log object + * + * @var object Log + */ + var $logger = null; + + /** + * Whether to enable logging of behaviour + * + * @var boolean + */ + var $enableLogging = false; + + /** + * Whether to regenerate session id everytime start is called + * + * @var boolean + */ + var $regenerateSessionId = false; + + // }}} + // {{{ Auth() [constructor] + + /** + * Constructor + * + * Set up the storage driver. + * + * @param string Type of the storage driver + * @param mixed Additional options for the storage driver + * (example: if you are using DB as the storage + * driver, you have to pass the dsn string here) + * + * @param string Name of the function that creates the login form + * @param boolean Should the login form be displayed if neccessary? + * @return void + */ + function Auth($storageDriver, $options = '', $loginFunction = '', $showLogin = true) + { + $this->applyAuthOptions($options); + + // Start the session suppress error if already started + if(!session_id()){ + @session_start(); + if(!session_id()) { + // Throw error + include_once 'PEAR.php'; + PEAR::throwError('Session could not be started by Auth, ' + .'possibly headers are already sent, try putting ' + .'ob_start in the beginning of your script'); + } + } + + // Make Sure Auth session variable is there + if(!isset($_SESSION[$this->_sessionName])) { + $_SESSION[$this->_sessionName] = array(); + } + + // Assign Some globals to internal references, this will replace _importGlobalVariable + $this->session =& $_SESSION[$this->_sessionName]; + $this->server =& $_SERVER; + $this->post =& $_POST; + $this->cookie =& $_COOKIE; + + if ($loginFunction != '' && is_callable($loginFunction)) { + $this->loginFunction = $loginFunction; + } + + if (is_bool($showLogin)) { + $this->showLogin = $showLogin; + } + + if (is_object($storageDriver)) { + $this->storage =& $storageDriver; + // Pass a reference to auth to the container, ugly but works + // this is used by the DB container to use method setAuthData not staticaly. + $this->storage->_auth_obj =& $this; + } else { + // $this->storage = $this->_factory($storageDriver, $options); + // + $this->storage_driver = $storageDriver; + $this->storage_options =& $options; + } + } + + // }}} + // {{{ applyAuthOptions() + + /** + * Set the Auth options + * + * Some options which are Auth specific will be applied + * the rest will be left for usage by the container + * + * @param array An array of Auth options + * @return array The options which were not applied + * @access private + */ + function &applyAuthOptions(&$options) + { + if(is_array($options)){ + if (!empty($options['sessionName'])) { + $this->_sessionName = $options['sessionName']; + unset($options['sessionName']); + } + if (isset($options['allowLogin'])) { + $this->allowLogin = $options['allowLogin']; + unset($options['allowLogin']); + } + if (!empty($options['postUsername'])) { + $this->_postUsername = $options['postUsername']; + unset($options['postUsername']); + } + if (!empty($options['postPassword'])) { + $this->_postPassword = $options['postPassword']; + unset($options['postPassword']); + } + if (isset($options['advancedsecurity'])) { + $this->advancedsecurity = $options['advancedsecurity']; + unset($options['advancedsecurity']); + } + if (isset($options['enableLogging'])) { + $this->enableLogging = $options['enableLogging']; + unset($options['enableLogging']); + } + if (isset($options['regenerateSessionId']) && is_bool($options['regenerateSessionId'])) { + $this->regenerateSessionId = $options['regenerateSessionId']; + } + } + return($options); + } + + // }}} + // {{{ _loadStorage() + + /** + * Load Storage Driver if not already loaded + * + * Suspend storage instantiation to make Auth lighter to use + * for calls which do not require login + * + * @return bool True if the conainer is loaded, false if the container + * is already loaded + * @access private + */ + function _loadStorage() + { + if(!is_object($this->storage)) { + $this->storage =& $this->_factory($this->storage_driver, + $this->storage_options); + $this->storage->_auth_obj =& $this; + $this->log('Loaded storage container ('.$this->storage_driver.')', AUTH_LOG_DEBUG); + return(true); + } + return(false); + } + + // }}} + // {{{ _factory() + + /** + * Return a storage driver based on $driver and $options + * + * @static + * @param string $driver Type of storage class to return + * @param string $options Optional parameters for the storage class + * @return object Object Storage object + * @access private + */ + function &_factory($driver, $options = '') + { + $storage_class = 'Auth_Container_' . $driver; + include_once 'Auth/Container/' . $driver . '.php'; + $obj =& new $storage_class($options); + return $obj; + } + + // }}} + // {{{ assignData() + + /** + * Assign data from login form to internal values + * + * This function takes the values for username and password + * from $HTTP_POST_VARS/$_POST and assigns them to internal variables. + * If you wish to use another source apart from $HTTP_POST_VARS/$_POST, + * you have to derive this function. + * + * @global $HTTP_POST_VARS, $_POST + * @see Auth + * @return void + * @access private + */ + function assignData() + { + $this->log('Auth::assignData() called.', AUTH_LOG_DEBUG); + + if ( isset($this->post[$this->_postUsername]) + && $this->post[$this->_postUsername] != '') { + $this->username = (get_magic_quotes_gpc() == 1 + ? stripslashes($this->post[$this->_postUsername]) + : $this->post[$this->_postUsername]); + } + if ( isset($this->post[$this->_postPassword]) + && $this->post[$this->_postPassword] != '') { + $this->password = (get_magic_quotes_gpc() == 1 + ? stripslashes($this->post[$this->_postPassword]) + : $this->post[$this->_postPassword] ); + } + } + + // }}} + // {{{ start() + + /** + * Start new auth session + * + * @return void + * @access public + */ + function start() + { + $this->log('Auth::start() called.', AUTH_LOG_DEBUG); + + // #10729 - Regenerate session id here if we are generating it on every + // page load. + if ($this->regenerateSessionId) { + session_regenerate_id(true); + } + + $this->assignData(); + if (!$this->checkAuth() && $this->allowLogin) { + $this->login(); + } + } + + // }}} + // {{{ login() + + /** + * Login function + * + * @return void + * @access private + */ + function login() + { + $this->log('Auth::login() called.', AUTH_LOG_DEBUG); + + $login_ok = false; + $this->_loadStorage(); + + // Check if using challenge response + (isset($this->post['authsecret']) && $this->post['authsecret'] == 1) + ? $usingChap = true + : $usingChap = false; + + + // When the user has already entered a username, we have to validate it. + if (!empty($this->username)) { + if (true === $this->storage->fetchData($this->username, $this->password, $usingChap)) { + $this->session['challengekey'] = md5($this->username.$this->password); + $login_ok = true; + $this->log('Successful login.', AUTH_LOG_INFO); + } + } + + if (!empty($this->username) && $login_ok) { + $this->setAuth($this->username); + if (is_callable($this->loginCallback)) { + $this->log('Calling loginCallback ('.$this->loginCallback.').', AUTH_LOG_DEBUG); + call_user_func_array($this->loginCallback, array($this->username, &$this)); + } + } + + // If the login failed or the user entered no username, + // output the login screen again. + if (!empty($this->username) && !$login_ok) { + $this->log('Incorrect login.', AUTH_LOG_INFO); + $this->status = AUTH_WRONG_LOGIN; + if (is_callable($this->loginFailedCallback)) { + $this->log('Calling loginFailedCallback ('.$this->loginFailedCallback.').', AUTH_LOG_DEBUG); + call_user_func_array($this->loginFailedCallback, array($this->username, &$this)); + } + } + + if ((empty($this->username) || !$login_ok) && $this->showLogin) { + $this->log('Rendering Login Form.', AUTH_LOG_INFO); + if (is_callable($this->loginFunction)) { + $this->log('Calling loginFunction ('.$this->loginFunction.').', AUTH_LOG_DEBUG); + call_user_func_array($this->loginFunction, array($this->username, $this->status, &$this)); + } else { + // BC fix Auth used to use drawLogin for this + // call is sub classes implement this + if (is_callable(array($this, 'drawLogin'))) { + $this->log('Calling Auth::drawLogin()', AUTH_LOG_DEBUG); + return $this->drawLogin($this->username, $this); + } + + $this->log('Using default Auth_Frontend_Html', AUTH_LOG_DEBUG); + + // New Login form + include_once 'Auth/Frontend/Html.php'; + return Auth_Frontend_Html::render($this, $this->username); + } + } else { + return; + } + } + + // }}} + // {{{ setExpire() + + /** + * Set the maximum expire time + * + * @param integer time in seconds + * @param bool add time to current expire time or not + * @return void + * @access public + */ + function setExpire($time, $add = false) + { + $add ? $this->expire += $time : $this->expire = $time; + } + + // }}} + // {{{ setIdle() + + /** + * Set the maximum idle time + * + * @param integer time in seconds + * @param bool add time to current maximum idle time or not + * @return void + * @access public + */ + function setIdle($time, $add = false) + { + $add ? $this->idle += $time : $this->idle = $time; + } + + // }}} + // {{{ setSessionName() + + /** + * Set name of the session to a customized value. + * + * If you are using multiple instances of PEAR::Auth + * on the same domain, you can change the name of + * session per application via this function. + * This will chnage the name of the session variable + * auth uses to store it's data in the session + * + * @param string New name for the session + * @return void + * @access public + */ + function setSessionName($name = 'session') + { + $this->_sessionName = '_auth_'.$name; + // Make Sure Auth session variable is there + if(!isset($_SESSION[$this->_sessionName])) { + $_SESSION[$this->_sessionName] = array(); + } + $this->session =& $_SESSION[$this->_sessionName]; + } + + // }}} + // {{{ setShowLogin() + + /** + * Should the login form be displayed if neccessary? + * + * @param bool show login form or not + * @return void + * @access public + */ + function setShowLogin($showLogin = true) + { + $this->showLogin = $showLogin; + } + + // }}} + // {{{ setAllowLogin() + + /** + * Should the login form be displayed if neccessary? + * + * @param bool show login form or not + * @return void + * @access public + */ + function setAllowLogin($allowLogin = true) + { + $this->allowLogin = $allowLogin; + } + + // }}} + // {{{ setCheckAuthCallback() + + /** + * Register a callback function to be called whenever the validity of the login is checked + * The function will receive two parameters, the username and a reference to the auth object. + * + * @param string callback function name + * @return void + * @access public + * @since Method available since Release 1.4.3 + */ + function setCheckAuthCallback($checkAuthCallback) + { + $this->checkAuthCallback = $checkAuthCallback; + } + + // }}} + // {{{ setLoginCallback() + + /** + * Register a callback function to be called on user login. + * The function will receive two parameters, the username and a reference to the auth object. + * + * @param string callback function name + * @return void + * @see setLogoutCallback() + * @access public + */ + function setLoginCallback($loginCallback) + { + $this->loginCallback = $loginCallback; + } + + // }}} + // {{{ setFailedLoginCallback() + + /** + * Register a callback function to be called on failed user login. + * The function will receive two parameters, the username and a reference to the auth object. + * + * @param string callback function name + * @return void + * @access public + */ + function setFailedLoginCallback($loginFailedCallback) + { + $this->loginFailedCallback = $loginFailedCallback; + } + + // }}} + // {{{ setLogoutCallback() + + /** + * Register a callback function to be called on user logout. + * The function will receive three parameters, the username and a reference to the auth object. + * + * @param string callback function name + * @return void + * @see setLoginCallback() + * @access public + */ + function setLogoutCallback($logoutCallback) + { + $this->logoutCallback = $logoutCallback; + } + + // }}} + // {{{ setAuthData() + + /** + * Register additional information that is to be stored + * in the session. + * + * @param string Name of the data field + * @param mixed Value of the data field + * @param boolean Should existing data be overwritten? (default + * is true) + * @return void + * @access public + */ + function setAuthData($name, $value, $overwrite = true) + { + if (!empty($this->session['data'][$name]) && $overwrite == false) { + return; + } + $this->session['data'][$name] = $value; + } + + // }}} + // {{{ getAuthData() + + /** + * Get additional information that is stored in the session. + * + * If no value for the first parameter is passed, the method will + * return all data that is currently stored. + * + * @param string Name of the data field + * @return mixed Value of the data field. + * @access public + */ + function getAuthData($name = null) + { + if (!isset($this->session['data'])) { + return null; + } + if(!isset($name)) { + return $this->session['data']; + } + if (isset($name) && isset($this->session['data'][$name])) { + return $this->session['data'][$name]; + } + return null; + } + + // }}} + // {{{ setAuth() + + /** + * Register variable in a session telling that the user + * has logged in successfully + * + * @param string Username + * @return void + * @access public + */ + function setAuth($username) + { + $this->log('Auth::setAuth() called.', AUTH_LOG_DEBUG); + + // #10729 - Regenerate session id here only if generating at login only + // Don't do it if we are regenerating on every request so we don't + // regenerate it twice in one request. + if (!$this->regenerateSessionId) { + // #2021 - Change the session id to avoid session fixation attacks php 4.3.3 > + session_regenerate_id(true); + } + + if (!isset($this->session) || !is_array($this->session)) { + $this->session = array(); + } + + if (!isset($this->session['data'])) { + $this->session['data'] = array(); + } + + $this->session['sessionip'] = isset($this->server['REMOTE_ADDR']) + ? $this->server['REMOTE_ADDR'] + : ''; + $this->session['sessionuseragent'] = isset($this->server['HTTP_USER_AGENT']) + ? $this->server['HTTP_USER_AGENT'] + : ''; + $this->session['sessionforwardedfor'] = isset($this->server['HTTP_X_FORWARDED_FOR']) + ? $this->server['HTTP_X_FORWARDED_FOR'] + : ''; + + // This should be set by the container to something more safe + // Like md5(passwd.microtime) + if(empty($this->session['challengekey'])) { + $this->session['challengekey'] = md5($username.microtime()); + } + + $this->session['challengecookie'] = md5($this->session['challengekey'].microtime()); + setcookie('authchallenge', $this->session['challengecookie']); + + $this->session['registered'] = true; + $this->session['username'] = $username; + $this->session['timestamp'] = time(); + $this->session['idle'] = time(); + } + + // }}} + // {{{ setAdvancedSecurity() + + /** + * Enables advanced security checks + * + * Currently only ip change and useragent change + * are detected + * @todo Add challenge cookies - Create a cookie which changes every time + * and contains some challenge key which the server can verify with + * a session var cookie might need to be crypted (user pass) + * @param bool Enable or disable + * @return void + * @access public + */ + function setAdvancedSecurity($flag=true) + { + $this->advancedsecurity = $flag; + } + + // }}} + // {{{ checkAuth() + + /** + * Checks if there is a session with valid auth information. + * + * @access public + * @return boolean Whether or not the user is authenticated. + */ + function checkAuth() + { + $this->log('Auth::checkAuth() called.', AUTH_LOG_DEBUG); + $this->authChecks++; + if (isset($this->session)) { + // Check if authentication session is expired + if ( $this->expire > 0 + && isset($this->session['timestamp']) + && ($this->session['timestamp'] + $this->expire) < time()) { + $this->log('Session Expired', AUTH_LOG_INFO); + $this->expired = true; + $this->status = AUTH_EXPIRED; + $this->logout(); + return false; + } + + // Check if maximum idle time is reached + if ( $this->idle > 0 + && isset($this->session['idle']) + && ($this->session['idle'] + $this->idle) < time()) { + $this->log('Session Idle Time Reached', AUTH_LOG_INFO); + $this->idled = true; + $this->status = AUTH_IDLED; + $this->logout(); + return false; + } + + if ( isset($this->session['registered']) + && isset($this->session['username']) + && $this->session['registered'] == true + && $this->session['username'] != '') { + Auth::updateIdle(); + + if ($this->advancedsecurity) { + $this->log('Advanced Security Mode Enabled.', AUTH_LOG_DEBUG); + + // Only Generate the challenge once + if($this->authChecks == 1) { + $this->log('Generating new Challenge Cookie.', AUTH_LOG_DEBUG); + $this->session['challengecookieold'] = $this->session['challengecookie']; + $this->session['challengecookie'] = md5($this->session['challengekey'].microtime()); + setcookie('authchallenge', $this->session['challengecookie']); + } + + // Check for ip change + if ( isset($this->server['REMOTE_ADDR']) + && $this->session['sessionip'] != $this->server['REMOTE_ADDR']) { + $this->log('Security Breach. Remote IP Address changed.', AUTH_LOG_INFO); + // Check if the IP of the user has changed, if so we + // assume a man in the middle attack and log him out + $this->expired = true; + $this->status = AUTH_SECURITY_BREACH; + $this->logout(); + return false; + } + + // Check for ip change (if connected via proxy) + if ( isset($this->server['HTTP_X_FORWARDED_FOR']) + && $this->session['sessionforwardedfor'] != $this->server['HTTP_X_FORWARDED_FOR']) { + $this->log('Security Breach. Forwarded For IP Address changed.', AUTH_LOG_INFO); + // Check if the IP of the user connecting via proxy has + // changed, if so we assume a man in the middle attack + // and log him out. + $this->expired = true; + $this->status = AUTH_SECURITY_BREACH; + $this->logout(); + return false; + } + + // Check for useragent change + if ( isset($this->server['HTTP_USER_AGENT']) + && $this->session['sessionuseragent'] != $this->server['HTTP_USER_AGENT']) { + $this->log('Security Breach. User Agent changed.', AUTH_LOG_INFO); + // Check if the User-Agent of the user has changed, if + // so we assume a man in the middle attack and log him out + $this->expired = true; + $this->status = AUTH_SECURITY_BREACH; + $this->logout(); + return false; + } + + // Check challenge cookie here, if challengecookieold is not set + // this is the first time and check is skipped + // TODO when user open two pages similtaneuly (open in new window,open + // in tab) auth breach is caused find out a way around that if possible + if ( isset($this->session['challengecookieold']) + && $this->session['challengecookieold'] != $this->cookie['authchallenge']) { + $this->log('Security Breach. Challenge Cookie mismatch.', AUTH_LOG_INFO); + $this->expired = true; + $this->status = AUTH_SECURITY_BREACH; + $this->logout(); + $this->login(); + return false; + } + } + + if (is_callable($this->checkAuthCallback)) { + $this->log('Calling checkAuthCallback ('.$this->checkAuthCallback.').', AUTH_LOG_DEBUG); + $checkCallback = call_user_func_array($this->checkAuthCallback, array($this->username, &$this)); + if ($checkCallback == false) { + $this->log('checkAuthCallback failed.', AUTH_LOG_INFO); + $this->expired = true; + $this->status = AUTH_CALLBACK_ABORT; + $this->logout(); + return false; + } + } + + $this->log('Session OK.', AUTH_LOG_INFO); + return true; + } + } + $this->log('Unable to locate session storage.', AUTH_LOG_DEBUG); + return false; + } + + // }}} + // {{{ staticCheckAuth() [static] + + /** + * Statically checks if there is a session with valid auth information. + * + * @access public + * @see checkAuth + * @return boolean Whether or not the user is authenticated. + * @static + */ + function staticCheckAuth($options = null) + { + static $staticAuth; + if(!isset($staticAuth)) { + $staticAuth = new Auth('null', $options); + } + $staticAuth->log('Auth::staticCheckAuth() called', AUTH_LOG_DEBUG); + return $staticAuth->checkAuth(); + } + + // }}} + // {{{ getAuth() + + /** + * Has the user been authenticated? + * + * @access public + * @return bool True if the user is logged in, otherwise false. + */ + function getAuth() + { + $this->log('Auth::getAuth() called.', AUTH_LOG_DEBUG); + return $this->checkAuth(); + } + + // }}} + // {{{ logout() + + /** + * Logout function + * + * This function clears any auth tokens in the currently + * active session and executes the logout callback function, + * if any + * + * @access public + * @return void + */ + function logout() + { + $this->log('Auth::logout() called.', AUTH_LOG_DEBUG); + + if (is_callable($this->logoutCallback) && isset($this->session['username'])) { + $this->log('Calling logoutCallback ('.$this->logoutCallback.').', AUTH_LOG_DEBUG); + call_user_func_array($this->logoutCallback, array($this->session['username'], &$this)); + } + + $this->username = ''; + $this->password = ''; + + $this->session = null; + } + + // }}} + // {{{ updateIdle() + + /** + * Update the idletime + * + * @access private + * @return void + */ + function updateIdle() + { + $this->session['idle'] = time(); + } + + // }}} + // {{{ getUsername() + + /** + * Get the username + * + * @return string + * @access public + */ + function getUsername() + { + if (isset($this->session['username'])) { + return($this->session['username']); + } + return(''); + } + + // }}} + // {{{ getStatus() + + /** + * Get the current status + * + * @return string + * @access public + */ + function getStatus() + { + return $this->status; + } + + // }}} + // {{{ getPostUsernameField() + + /** + * Gets the post varible used for the username + * + * @return string + * @access public + */ + function getPostUsernameField() + { + return($this->_postUsername); + } + + // }}} + // {{{ getPostPasswordField() + + /** + * Gets the post varible used for the username + * + * @return string + * @access public + */ + function getPostPasswordField() + { + return($this->_postPassword); + } + + // }}} + // {{{ sessionValidThru() + + /** + * Returns the time up to the session is valid + * + * @access public + * @return integer + */ + function sessionValidThru() + { + if (!isset($this->session['idle'])) { + return 0; + } + if ($this->idle == 0) { + return 0; + } + return ($this->session['idle'] + $this->idle); + } + + // }}} + // {{{ listUsers() + + /** + * List all users that are currently available in the storage + * container + * + * @access public + * @return array + */ + function listUsers() + { + $this->log('Auth::listUsers() called.', AUTH_LOG_DEBUG); + $this->_loadStorage(); + return $this->storage->listUsers(); + } + + // }}} + // {{{ addUser() + + /** + * Add user to the storage container + * + * @access public + * @param string Username + * @param string Password + * @param mixed Additional parameters + * @return mixed True on success, PEAR error object on error + * and AUTH_METHOD_NOT_SUPPORTED otherwise. + */ + function addUser($username, $password, $additional = '') + { + $this->log('Auth::addUser() called.', AUTH_LOG_DEBUG); + $this->_loadStorage(); + return $this->storage->addUser($username, $password, $additional); + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @access public + * @param string Username + * @return mixed True on success, PEAR error object on error + * and AUTH_METHOD_NOT_SUPPORTED otherwise. + */ + function removeUser($username) + { + $this->log('Auth::removeUser() called.', AUTH_LOG_DEBUG); + $this->_loadStorage(); + return $this->storage->removeUser($username); + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @access public + * @param string Username + * @param string The new password + * @return mixed True on success, PEAR error object on error + * and AUTH_METHOD_NOT_SUPPORTED otherwise. + */ + function changePassword($username, $password) + { + $this->log('Auth::changePassword() called', AUTH_LOG_DEBUG); + $this->_loadStorage(); + return $this->storage->changePassword($username, $password); + } + + // }}} + // {{{ log() + + /** + * Log a message from the Auth system + * + * @access public + * @param string The message to log + * @param string The log level to log the message under. See the Log documentation for more info. + * @return boolean + */ + function log($message, $level = AUTH_LOG_DEBUG) + { + if (!$this->enableLogging) return false; + + $this->_loadLogger(); + + $this->logger->log('AUTH: '.$message, $level); + } + + // }}} + // {{{ _loadLogger() + + /** + * Load Log object if not already loaded + * + * Suspend logger instantiation to make Auth lighter to use + * for calls which do not require logging + * + * @return bool True if the logger is loaded, false if the logger + * is already loaded + * @access private + */ + function _loadLogger() + { + if(is_null($this->logger)) { + if (!class_exists('Log')) { + include_once 'Log.php'; + } + $this->logger =& Log::singleton('null', + null, + 'auth['.getmypid().']', + array(), + AUTH_LOG_DEBUG); + return(true); + } + return(false); + } + + // }}} + // {{{ attachLogObserver() + + /** + * Attach an Observer to the Auth Log Source + * + * @param object Log_Observer A Log Observer instance + * @return boolean + */ + function attachLogObserver(&$observer) { + + $this->_loadLogger(); + + return $this->logger->attach($observer); + + } + + // }}} + +} +?> diff --git a/Auth/Anonymous.php b/Auth/Anonymous.php new file mode 100644 index 0000000..f7da535 --- /dev/null +++ b/Auth/Anonymous.php @@ -0,0 +1,138 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Anonymous.php,v 1.6 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Include Auth package + */ +require_once 'Auth.php'; + +/** + * Anonymous Authentication + * + * This class provides anonymous authentication if username and password + * were not supplied + * + * @category Authentication + * @package Auth + * @author Yavor Shahpasov + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.6 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Anonymous extends Auth +{ + + // {{{ properties + + /** + * Whether to allow anonymous authentication + * + * @var boolean + */ + var $allow_anonymous = true; + + /** + * Username to use for anonymous user + * + * @var string + */ + var $anonymous_username = 'anonymous'; + + // }}} + // {{{ Auth_Anonymous() [constructor] + + /** + * Pass all parameters to Parent Auth class + * + * Set up the storage driver. + * + * @param string Type of the storage driver + * @param mixed Additional options for the storage driver + * (example: if you are using DB as the storage + * driver, you have to pass the dsn string here) + * + * @param string Name of the function that creates the login form + * @param boolean Should the login form be displayed if neccessary? + * @return void + * @see Auth::Auth() + */ + function Auth_Anonymous($storageDriver, $options = '', $loginFunction = '', $showLogin = true) { + parent::Auth($storageDriver, $options, $loginFunction, $showLogin); + } + + // }}} + // {{{ login() + + /** + * Login function + * + * If no username & password is passed then login as the username + * provided in $this->anonymous_username else call standard login() + * function. + * + * @return void + * @access private + * @see Auth::login() + */ + function login() { + if ( $this->allow_anonymous + && empty($this->username) + && empty($this->password) ) { + $this->setAuth($this->anonymous_username); + if (is_callable($this->loginCallback)) { + call_user_func_array($this->loginCallback, array($this->username, $this) ); + } + } else { + // Call normal login system + parent::login(); + } + } + + // }}} + // {{{ forceLogin() + + /** + * Force the user to login + * + * Calling this function forces the user to provide a real username and + * password before continuing. + * + * @return void + */ + function forceLogin() { + $this->allow_anonymous = false; + if( !empty($this->session['username']) && $this->session['username'] == $this->anonymous_username ) { + $this->logout(); + } + } + + // }}} + +} + +?> diff --git a/Auth/Auth.php b/Auth/Auth.php new file mode 100644 index 0000000..78c7f5d --- /dev/null +++ b/Auth/Auth.php @@ -0,0 +1,30 @@ + + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Auth.php,v 1.4 2006/03/02 06:53:08 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @deprecated File deprecated since Release 1.2.0 + */ + +/** + * Include Auth package + */ +require_once 'Auth.php'; + +?> diff --git a/Auth/Container.php b/Auth/Container.php new file mode 100644 index 0000000..1816137 --- /dev/null +++ b/Auth/Container.php @@ -0,0 +1,262 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Container.php,v 1.28 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + */ + +/** + * Storage class for fetching login data + * + * @category Authentication + * @package Auth + * @author Martin Jansen + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.28 $ + * @link http://pear.php.net/package/Auth + */ +class Auth_Container +{ + + // {{{ properties + + /** + * User that is currently selected from the storage container. + * + * @access public + */ + var $activeUser = ""; + + /** + * The Auth object this container is attached to. + * + * @access public + */ + var $_auth_obj = null; + + // }}} + // {{{ Auth_Container() [constructor] + + /** + * Constructor + * + * Has to be overwritten by each storage class + * + * @access public + */ + function Auth_Container() + { + } + + // }}} + // {{{ fetchData() + + /** + * Fetch data from storage container + * + * Has to be overwritten by each storage class + * + * @access public + */ + function fetchData($username, $password, $isChallengeResponse=false) + { + $this->log('Auth_Container::fetchData() called.', AUTH_LOG_DEBUG); + } + + // }}} + // {{{ verifyPassword() + + /** + * Crypt and verfiy the entered password + * + * @param string Entered password + * @param string Password from the data container (usually this password + * is already encrypted. + * @param string Type of algorithm with which the password from + * the container has been crypted. (md5, crypt etc.) + * Defaults to "md5". + * @return bool True, if the passwords match + */ + function verifyPassword($password1, $password2, $cryptType = "md5") + { + $this->log('Auth_Container::verifyPassword() called.', AUTH_LOG_DEBUG); + switch ($cryptType) { + case "crypt" : + return ((string)crypt($password1, $password2) === (string)$password2); + break; + case "none" : + case "" : + return ((string)$password1 === (string)$password2); + break; + case "md5" : + return ((string)md5($password1) === (string)$password2); + break; + default : + if (function_exists($cryptType)) { + return ((string)$cryptType($password1) === (string)$password2); + } elseif (method_exists($this,$cryptType)) { + return ((string)$this->$cryptType($password1) === (string)$password2); + } else { + return false; + } + break; + } + } + + // }}} + // {{{ supportsChallengeResponse() + + /** + * Returns true if the container supports Challenge Response + * password authentication + */ + function supportsChallengeResponse() + { + return(false); + } + + // }}} + // {{{ getCryptType() + + /** + * Returns the crypt current crypt type of the container + * + * @return string + */ + function getCryptType() + { + return(''); + } + + // }}} + // {{{ listUsers() + + /** + * List all users that are available from the storage container + */ + function listUsers() + { + $this->log('Auth_Container::listUsers() called.', AUTH_LOG_DEBUG); + return AUTH_METHOD_NOT_SUPPORTED; + } + + // }}} + // {{{ getUser() + + /** + * Returns a user assoc array + * + * Containers which want should overide this + * + * @param string The username + */ + function getUser($username) + { + $this->log('Auth_Container::getUser() called.', AUTH_LOG_DEBUG); + $users = $this->listUsers(); + if ($users === AUTH_METHOD_NOT_SUPPORTED) { + return AUTH_METHOD_NOT_SUPPORTED; + } + for ($i=0; $c = count($users), $i<$c; $i++) { + if ($users[$i]['username'] == $username) { + return $users[$i]; + } + } + return false; + } + + // }}} + // {{{ addUser() + + /** + * Add a new user to the storage container + * + * @param string Username + * @param string Password + * @param array Additional information + * + * @return boolean + */ + function addUser($username, $password, $additional=null) + { + $this->log('Auth_Container::addUser() called.', AUTH_LOG_DEBUG); + return AUTH_METHOD_NOT_SUPPORTED; + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @param string Username + */ + function removeUser($username) + { + $this->log('Auth_Container::removeUser() called.', AUTH_LOG_DEBUG); + return AUTH_METHOD_NOT_SUPPORTED; + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password + */ + function changePassword($username, $password) + { + $this->log('Auth_Container::changePassword() called.', AUTH_LOG_DEBUG); + return AUTH_METHOD_NOT_SUPPORTED; + } + + // }}} + // {{{ log() + + /** + * Log a message to the Auth log + * + * @param string The message + * @param int + * @return boolean + */ + function log($message, $level = AUTH_LOG_DEBUG) { + + if (is_null($this->_auth_obj)) { + + return false; + + } else { + + return $this->_auth_obj->log($message, $level); + + } + + } + + // }}} + +} + +?> diff --git a/Auth/Container/Array.php b/Auth/Container/Array.php new file mode 100644 index 0000000..c16a12a --- /dev/null +++ b/Auth/Container/Array.php @@ -0,0 +1,161 @@ + + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Array.php,v 1.5 2007/06/12 03:11:26 aashley Exp $ + * @since File available since Release 1.4.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching authentication data from a PHP Array + * + * This container takes two options when configuring: + * + * cryptType: The crypt used to store the password. Currently recognised + * are: none, md5 and crypt. default: none + * users: A named array of usernames and passwords. + * Ex: + * array( + * 'guest' => '084e0343a0486ff05530df6c705c8bb4', // password guest + * 'georg' => 'fc77dba827fcc88e0243404572c51325' // password georg + * ) + * + * Usage Example: + * array( + * 'guest' => '084e0343a0486ff05530df6c705c8bb4', // password guest + * 'georg' => 'fc77dba827fcc88e0243404572c51325' // password georg + * ), + * 'cryptType'=>'md5', + * ); + * + * $auth = new Auth("Array", $AuthOptions); + * ?> + * + * @category Authentication + * @package Auth + * @author georg_1 at have2 dot com + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.5 $ + * @since File available since Release 1.4.0 + */ + +class Auth_Container_Array extends Auth_Container { + + // {{{ properties + + /** + * The users and their password to authenticate against + * + * @var array $users + */ + var $users; + + /** + * The cryptType used on the passwords + * + * @var string $cryptType + */ + var $cryptType = 'none'; + + // }}} + // {{{ Auth_Container_Array() + + /** + * Constructor for Array Container + * + * @param array $data Options for the container + * @return void + */ + function Auth_Container_Array($data) + { + if (!is_array($data)) { + PEAR::raiseError('The options for Auth_Container_Array must be an array'); + } + if (isset($data['users']) && is_array($data['users'])) { + $this->users = $data['users']; + } else { + $this->users = array(); + PEAR::raiseError('Auth_Container_Array: no user data found in options array'); + } + if (isset($data['cryptType'])) { + $this->cryptType = $data['cryptType']; + } + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from array + * + * This function uses the given username to fetch the corresponding + * login data from the array. If an account that matches the passed + * username and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @return boolean|PEAR_Error Error object or boolean + */ + function fetchData($user, $pass) + { + $this->log('Auth_Container_Array::fetchData() called.', AUTH_LOG_DEBUG); + if ( isset($this->users[$user]) + && $this->verifyPassword($pass, $this->users[$user], $this->cryptType)) { + return true; + } + return false; + } + + // }}} + // {{{ listUsers() + + /** + * Returns a list of users available within the container + * + * @return array + */ + function listUsers() + { + $this->log('Auth_Container_Array::listUsers() called.', AUTH_LOG_DEBUG); + $ret = array(); + foreach ($this->users as $username => $password) { + $ret[]['username'] = $username; + } + return $ret; + } + + // }}} + +} + +?> diff --git a/Auth/Container/DB.php b/Auth/Container/DB.php new file mode 100644 index 0000000..1e33a16 --- /dev/null +++ b/Auth/Container/DB.php @@ -0,0 +1,632 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: DB.php,v 1.72 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR DB + */ +require_once 'DB.php'; + +/** + * Storage driver for fetching login data from a database + * + * This storage driver can use all databases which are supported + * by the PEAR DB abstraction layer to fetch login data. + * + * @category Authentication + * @package Auth + * @author Martin Jansen + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.72 $ + * @link http://pear.php.net/package/Auth + */ +class Auth_Container_DB extends Auth_Container +{ + + // {{{ properties + + /** + * Additional options for the storage container + * @var array + */ + var $options = array(); + + /** + * DB object + * @var object + */ + var $db = null; + var $dsn = ''; + + /** + * User that is currently selected from the DB. + * @var string + */ + var $activeUser = ''; + + // }}} + // {{{ Auth_Container_DB [constructor] + + /** + * Constructor of the container class + * + * Save the initial options passed to the container. Initiation of the DB + * connection is no longer performed here and is only done when needed. + * + * @param string Connection data or DB object + * @return object Returns an error object if something went wrong + */ + function Auth_Container_DB($dsn) + { + $this->_setDefaults(); + + if (is_array($dsn)) { + $this->_parseOptions($dsn); + + if (empty($this->options['dsn'])) { + PEAR::raiseError('No connection parameters specified!'); + } + } else { + $this->options['dsn'] = $dsn; + } + } + + // }}} + // {{{ _connect() + + /** + * Connect to database by using the given DSN string + * + * @access private + * @param string DSN string + * @return mixed Object on error, otherwise bool + */ + function _connect($dsn) + { + $this->log('Auth_Container_DB::_connect() called.', AUTH_LOG_DEBUG); + + if (is_string($dsn) || is_array($dsn)) { + $this->db = DB::Connect($dsn, $this->options['db_options']); + } elseif (is_subclass_of($dsn, 'db_common')) { + $this->db = $dsn; + } elseif (DB::isError($dsn)) { + return PEAR::raiseError($dsn->getMessage(), $dsn->getCode()); + } else { + return PEAR::raiseError('The given dsn was not valid in file ' . __FILE__ . ' at line ' . __LINE__, + 41, + PEAR_ERROR_RETURN, + null, + null + ); + } + + if (DB::isError($this->db) || PEAR::isError($this->db)) { + return PEAR::raiseError($this->db->getMessage(), $this->db->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ _prepare() + + /** + * Prepare database connection + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a DB error object. + */ + function _prepare() + { + if (!DB::isConnection($this->db)) { + $res = $this->_connect($this->options['dsn']); + if (DB::isError($res) || PEAR::isError($res)) { + return $res; + } + } + if ($this->options['auto_quote'] && $this->db->dsn['phptype'] != 'sqlite') { + $this->options['final_table'] = $this->db->quoteIdentifier($this->options['table']); + $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol']); + $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol']); + } else { + $this->options['final_table'] = $this->options['table']; + $this->options['final_usernamecol'] = $this->options['usernamecol']; + $this->options['final_passwordcol'] = $this->options['passwordcol']; + } + return true; + } + + // }}} + // {{{ query() + + /** + * Prepare query to the database + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * After that the query is passed to the database. + * + * @access public + * @param string Query string + * @return mixed a DB_result object or DB_OK on success, a DB + * or PEAR error on failure + */ + function query($query) + { + $err = $this->_prepare(); + if ($err !== true) { + return $err; + } + return $this->db->query($query); + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->options['table'] = 'auth'; + $this->options['usernamecol'] = 'username'; + $this->options['passwordcol'] = 'password'; + $this->options['dsn'] = ''; + $this->options['db_fields'] = ''; + $this->options['cryptType'] = 'md5'; + $this->options['db_options'] = array(); + $this->options['db_where'] = ''; + $this->options['auto_quote'] = true; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + } + + // }}} + // {{{ _quoteDBFields() + + /** + * Quote the db_fields option to avoid the possibility of SQL injection. + * + * @access private + * @return string A properly quoted string that can be concatenated into a + * SELECT clause. + */ + function _quoteDBFields() + { + if (isset($this->options['db_fields'])) { + if (is_array($this->options['db_fields'])) { + if ($this->options['auto_quote']) { + $fields = array(); + foreach ($this->options['db_fields'] as $field) { + $fields[] = $this->db->quoteIdentifier($field); + } + return implode(', ', $fields); + } else { + return implode(', ', $this->options['db_fields']); + } + } else { + if (strlen($this->options['db_fields']) > 0) { + if ($this->options['auto_quote']) { + return $this->db->quoteIdentifier($this->options['db_fields']); + } else { + return $this->options['db_fields']; + } + } + } + } + + return ''; + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from database + * + * This function uses the given username to fetch + * the corresponding login data from the database + * table. If an account that matches the passed username + * and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @param boolean If true password is secured using a md5 hash + * the frontend and auth are responsible for making sure the container supports + * challenge response password authentication + * @return mixed Error object or boolean + */ + function fetchData($username, $password, $isChallengeResponse=false) + { + $this->log('Auth_Container_DB::fetchData() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // Find if db_fields contains a *, if so assume all columns are selected + if (is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = "*"; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = "SELECT ".$sql_from. + " FROM ".$this->options['final_table']. + " WHERE ".$this->options['final_usernamecol']." = ".$this->db->quoteSmart($username); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->getRow($query, null, DB_FETCHMODE_ASSOC); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } + + if (!is_array($res)) { + $this->activeUser = ''; + return false; + } + + // Perform trimming here before the hashihg + $password = trim($password, "\r\n"); + $res[$this->options['passwordcol']] = trim($res[$this->options['passwordcol']], "\r\n"); + + // If using Challenge Response md5 the pass with the secret + if ($isChallengeResponse) { + $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']] + .$this->_auth_obj->session['loginchallenege']); + + // UGLY cannot avoid without modifying verifyPassword + if ($this->options['cryptType'] == 'md5') { + $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']]); + } + + //print " Hashed Password [{$res[$this->options['passwordcol']]}]
\n"; + } + + if ($this->verifyPassword($password, + $res[$this->options['passwordcol']], + $this->options['cryptType'])) { + // Store additional field values in the session + foreach ($res as $key => $value) { + if ($key == $this->options['passwordcol'] || + $key == $this->options['usernamecol']) { + continue; + } + + $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG); + + // Use reference to the auth object if exists + // This is because the auth session variable can change so a + // static call to setAuthData does not make sence + $this->_auth_obj->setAuthData($key, $value); + } + return true; + } + $this->activeUser = $res[$this->options['usernamecol']]; + return false; + } + + // }}} + // {{{ listUsers() + + /** + * Returns a list of users from the container + * + * @return mixed + * @access public + */ + function listUsers() + { + $this->log('Auth_Container_DB::listUsers() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $retVal = array(); + + // Find if db_fields contains a *, if so assume all col are selected + if ( is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = "*"; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = sprintf("SELECT %s FROM %s", + $sql_from, + $this->options['final_table'] + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " WHERE ".$this->options['db_where']; + } + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->getAll($query, null, DB_FETCHMODE_ASSOC); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + foreach ($res as $user) { + $user['username'] = $user[$this->options['usernamecol']]; + $retVal[] = $user; + } + } + $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG); + return $retVal; + } + + // }}} + // {{{ addUser() + + /** + * Add user to the storage container + * + * @access public + * @param string Username + * @param string Password + * @param mixed Additional information that are stored in the DB + * + * @return mixed True on success, otherwise error object + */ + function addUser($username, $password, $additional = "") + { + $this->log('Auth_Container_DB::addUser() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if ( isset($this->options['cryptType']) + && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif ( isset($this->options['cryptType']) + && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + $additional_key = ''; + $additional_value = ''; + + if (is_array($additional)) { + foreach ($additional as $key => $value) { + if ($this->options['auto_quote']) { + $additional_key .= ', ' . $this->db->quoteIdentifier($key); + } else { + $additional_key .= ', ' . $key; + } + $additional_value .= ", " . $this->db->quoteSmart($value); + } + } + + $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES (%s, %s%s)", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->options['final_passwordcol'], + $additional_key, + $this->db->quoteSmart($username), + $this->db->quoteSmart($password), + $additional_value + ); + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @access public + * @param string Username + * + * @return mixed True on success, otherwise error object + */ + function removeUser($username) + { + $this->log('Auth_Container_DB::removeUser() called.', AUTH_LOG_DEBUG); + + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $where = " AND ".$this->options['db_where']; + } else { + $where = ''; + } + + $query = sprintf("DELETE FROM %s WHERE %s = %s %s", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->db->quoteSmart($username), + $where + ); + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password (plain text) + */ + function changePassword($username, $password) + { + $this->log('Auth_Container_DB::changePassword() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if ( isset($this->options['cryptType']) + && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif ( isset($this->options['cryptType']) + && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $where = " AND ".$this->options['db_where']; + } else { + $where = ''; + } + + $query = sprintf("UPDATE %s SET %s = %s WHERE %s = %s %s", + $this->options['final_table'], + $this->options['final_passwordcol'], + $this->db->quoteSmart($password), + $this->options['final_usernamecol'], + $this->db->quoteSmart($username), + $where + ); + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ supportsChallengeResponse() + + /** + * Determine if this container supports + * password authentication with challenge response + * + * @return bool + * @access public + */ + function supportsChallengeResponse() + { + return in_array($this->options['cryptType'], array('md5', 'none', '')); + } + + // }}} + // {{{ getCryptType() + + /** + * Returns the selected crypt type for this container + */ + function getCryptType() + { + return($this->options['cryptType']); + } + + // }}} + +} +?> diff --git a/Auth/Container/DBLite.php b/Auth/Container/DBLite.php new file mode 100644 index 0000000..759af97 --- /dev/null +++ b/Auth/Container/DBLite.php @@ -0,0 +1,313 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: DBLite.php,v 1.18 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR DB package + */ +require_once 'DB.php'; + +/** + * A lighter storage driver for fetching login data from a database + * + * This driver is derived from the DB storage container but + * with the user manipulation function removed for smaller file size + * by the PEAR DB abstraction layer to fetch login data. + * + * @category Authentication + * @package Auth + * @author Martin Jansen + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.18 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Container_DBLite extends Auth_Container +{ + + // {{{ properties + + /** + * Additional options for the storage container + * @var array + */ + var $options = array(); + + /** + * DB object + * @var object + */ + var $db = null; + var $dsn = ''; + + /** + * User that is currently selected from the DB. + * @var string + */ + var $activeUser = ''; + + // }}} + // {{{ Auth_Container_DBLite() [constructor] + + /** + * Constructor of the container class + * + * Initate connection to the database via PEAR::DB + * + * @param string Connection data or DB object + * @return object Returns an error object if something went wrong + */ + function Auth_Container_DBLite($dsn) + { + $this->options['table'] = 'auth'; + $this->options['usernamecol'] = 'username'; + $this->options['passwordcol'] = 'password'; + $this->options['dsn'] = ''; + $this->options['db_fields'] = ''; + $this->options['cryptType'] = 'md5'; + $this->options['db_options'] = array(); + $this->options['db_where'] = ''; + $this->options['auto_quote'] = true; + + if (is_array($dsn)) { + $this->_parseOptions($dsn); + if (empty($this->options['dsn'])) { + PEAR::raiseError('No connection parameters specified!'); + } + } else { + $this->options['dsn'] = $dsn; + } + } + + // }}} + // {{{ _connect() + + /** + * Connect to database by using the given DSN string + * + * @access private + * @param string DSN string + * @return mixed Object on error, otherwise bool + */ + function _connect(&$dsn) + { + $this->log('Auth_Container_DBLite::_connect() called.', AUTH_LOG_DEBUG); + if (is_string($dsn) || is_array($dsn)) { + $this->db =& DB::connect($dsn, $this->options['db_options']); + } elseif (is_subclass_of($dsn, "db_common")) { + $this->db =& $dsn; + } else { + return PEAR::raiseError("Invalid dsn or db object given"); + } + + if (DB::isError($this->db) || PEAR::isError($this->db)) { + return PEAR::raiseError($this->db->getMessage(), $this->db->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ _prepare() + + /** + * Prepare database connection + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a DB error object. + */ + function _prepare() + { + if (!DB::isConnection($this->db)) { + $res = $this->_connect($this->options['dsn']); + if (DB::isError($res) || PEAR::isError($res)) { + return $res; + } + } + if ($this->options['auto_quote'] && $this->db->dsn['phptype'] != 'sqlite') { + $this->options['final_table'] = $this->db->quoteIdentifier($this->options['table']); + $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol']); + $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol']); + } else { + $this->options['final_table'] = $this->options['table']; + $this->options['final_usernamecol'] = $this->options['usernamecol']; + $this->options['final_passwordcol'] = $this->options['passwordcol']; + } + return true; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + } + + // }}} + // {{{ _quoteDBFields() + + /** + * Quote the db_fields option to avoid the possibility of SQL injection. + * + * @access private + * @return string A properly quoted string that can be concatenated into a + * SELECT clause. + */ + function _quoteDBFields() + { + if (isset($this->options['db_fields'])) { + if (is_array($this->options['db_fields'])) { + if ($this->options['auto_quote']) { + $fields = array(); + foreach ($this->options['db_fields'] as $field) { + $fields[] = $this->db->quoteIdentifier($field); + } + return implode(', ', $fields); + } else { + return implode(', ', $this->options['db_fields']); + } + } else { + if (strlen($this->options['db_fields']) > 0) { + if ($this->options['auto_quote']) { + return $this->db->quoteIdentifier($this->options['db_fields']); + } else { + $this->options['db_fields']; + } + } + } + } + + return ''; + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from database + * + * This function uses the given username to fetch + * the corresponding login data from the database + * table. If an account that matches the passed username + * and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @return mixed Error object or boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_DBLite::fetchData() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // Find if db_fields contains a *, if so assume all col are selected + if (is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = "*"; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = "SELECT ".$sql_from. + " FROM ".$this->options['final_table']. + " WHERE ".$this->options['final_usernamecol']." = ".$this->db->quoteSmart($username); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->getRow($query, null, DB_FETCHMODE_ASSOC); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } + if (!is_array($res)) { + $this->activeUser = ''; + return false; + } + if ($this->verifyPassword(trim($password, "\r\n"), + trim($res[$this->options['passwordcol']], "\r\n"), + $this->options['cryptType'])) { + // Store additional field values in the session + foreach ($res as $key => $value) { + if ($key == $this->options['passwordcol'] || + $key == $this->options['usernamecol']) { + continue; + } + + $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG); + + // Use reference to the auth object if exists + // This is because the auth session variable can change so a static call to setAuthData does not make sence + if (is_object($this->_auth_obj)) { + $this->_auth_obj->setAuthData($key, $value); + } else { + Auth::setAuthData($key, $value); + } + } + $this->activeUser = $res[$this->options['usernamecol']]; + return true; + } + $this->activeUser = $res[$this->options['usernamecol']]; + return false; + } + + // }}} + +} +?> diff --git a/Auth/Container/File.php b/Auth/Container/File.php new file mode 100644 index 0000000..716cb3f --- /dev/null +++ b/Auth/Container/File.php @@ -0,0 +1,314 @@ + + * @author Martin Jansen + * @author Mika Tuupola + * @author Michael Wallner + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: File.php,v 1.25 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + */ + +/** + * Include PEAR File_Passwd package + */ +require_once "File/Passwd.php"; +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from an encrypted password file. + * + * This storage container can handle CVS pserver style passwd files. + * + * @category Authentication + * @package Auth + * @author Stefan Ekman + * @author Martin Jansen + * @author Mika Tuupola + * @author Michael Wallner + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.25 $ + * @link http://pear.php.net/package/Auth + */ +class Auth_Container_File extends Auth_Container +{ + + // {{{ properties + + /** + * Path to passwd file + * + * @var string + */ + var $pwfile = ''; + + /** + * Options for container + * + * @var array + */ + var $options = array(); + + // }}} + // {{{ Auth_Container_File() [constructor] + + /** + * Constructor of the container class + * + * @param string $filename path to passwd file + * @return object Auth_Container_File new Auth_Container_File object + */ + function Auth_Container_File($filename) { + $this->_setDefaults(); + + // Only file is a valid option here + if(is_array($filename)) { + $this->pwfile = $filename['file']; + $this->_parseOptions($filename); + } else { + $this->pwfile = $filename; + } + } + + // }}} + // {{{ fetchData() + + /** + * Authenticate an user + * + * @param string username + * @param string password + * @return mixed boolean|PEAR_Error + */ + function fetchData($user, $pass) + { + $this->log('Auth_Container_File::fetchData() called.', AUTH_LOG_DEBUG); + return File_Passwd::staticAuth($this->options['type'], $this->pwfile, $user, $pass); + } + + // }}} + // {{{ listUsers() + + /** + * List all available users + * + * @return array + */ + function listUsers() + { + $this->log('Auth_Container_File::listUsers() called.', AUTH_LOG_DEBUG); + + $pw_obj = &$this->_load(); + if (PEAR::isError($pw_obj)) { + return array(); + } + + $users = $pw_obj->listUser(); + if (!is_array($users)) { + return array(); + } + + foreach ($users as $key => $value) { + $retVal[] = array("username" => $key, + "password" => $value['passwd'], + "cvsuser" => $value['system']); + } + + $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG); + + return $retVal; + } + + // }}} + // {{{ addUser() + + /** + * Add a new user to the storage container + * + * @param string username + * @param string password + * @param mixed Additional parameters to File_Password_*::addUser() + * + * @return boolean + */ + function addUser($user, $pass, $additional='') + { + $this->log('Auth_Container_File::addUser() called.', AUTH_LOG_DEBUG); + $params = array($user, $pass); + if (is_array($additional)) { + foreach ($additional as $item) { + $params[] = $item; + } + } else { + $params[] = $additional; + } + + $pw_obj = &$this->_load(); + if (PEAR::isError($pw_obj)) { + return false; + } + + $res = call_user_func_array(array(&$pw_obj, 'addUser'), $params); + if (PEAR::isError($res)) { + return false; + } + + $res = $pw_obj->save(); + if (PEAR::isError($res)) { + return false; + } + + return true; + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @param string Username + * @return boolean + */ + function removeUser($user) + { + $this->log('Auth_Container_File::removeUser() called.', AUTH_LOG_DEBUG); + $pw_obj = &$this->_load(); + if (PEAR::isError($pw_obj)) { + return false; + } + + $res = $pw_obj->delUser($user); + if (PEAR::isError($res)) { + return false; + } + + $res = $pw_obj->save(); + if (PEAR::isError($res)) { + return false; + } + + return true; + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password + */ + function changePassword($username, $password) + { + $this->log('Auth_Container_File::changePassword() called.', AUTH_LOG_DEBUG); + $pw_obj = &$this->_load(); + if (PEAR::isError($pw_obj)) { + return false; + } + + $res = $pw_obj->changePasswd($username, $password); + if (PEAR::isError($res)) { + return false; + } + + $res = $pw_obj->save(); + if (PEAR::isError($res)) { + return false; + } + + return true; + } + + // }}} + // {{{ _load() + + /** + * Load and initialize the File_Passwd object + * + * @return object File_Passwd_Cvs|PEAR_Error + */ + function &_load() + { + static $pw_obj; + + if (!isset($pw_obj)) { + $this->log('Instanciating File_Password object of type '.$this->options['type'], AUTH_LOG_DEBUG); + $pw_obj = File_Passwd::factory($this->options['type']); + if (PEAR::isError($pw_obj)) { + return $pw_obj; + } + + $pw_obj->setFile($this->pwfile); + + $res = $pw_obj->load(); + if (PEAR::isError($res)) { + return $res; + } + } + + return $pw_obj; + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->options['type'] = 'Cvs'; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + } + + // }}} + +} +?> diff --git a/Auth/Container/IMAP.php b/Auth/Container/IMAP.php new file mode 100644 index 0000000..bfd31e2 --- /dev/null +++ b/Auth/Container/IMAP.php @@ -0,0 +1,210 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: IMAP.php,v 1.18 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; + +/** + * Include PEAR class for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from an IMAP server + * + * This class is based on LDAP containers, but it very simple. + * By default it connects to localhost:143 + * The constructor will first check if the host:port combination is + * actually reachable. This behaviour can be disabled. + * It then tries to create an IMAP stream (without opening a mailbox) + * If you wish to pass extended options to the connections, you may + * do so by specifying protocol options. + * + * To use this storage containers, you have to use the + * following syntax: + * + * 'mail.example.com', + * 'port' => 143, + * ); + * $myAuth = new Auth('IMAP', $params); + * ... + * + * By default we connect without any protocol options set. However, some + * servers require you to connect with the notls or norsh options set. + * To do this you need to add the following value to the params array: + * 'baseDSN' => '/imap/notls/norsh' + * + * To connect to an SSL IMAP server: + * 'baseDSN' => '/imap/ssl' + * + * To connect to an SSL IMAP server with a self-signed certificate: + * 'baseDSN' => '/imap/ssl/novalidate-cert' + * + * Further options may be available and can be found on the php site at + * http://www.php.net/manual/function.imap-open.php + * + * @category Authentication + * @package Auth + * @author Jeroen Houben + * @author Cipriano Groenendal + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.18 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.0 + */ +class Auth_Container_IMAP extends Auth_Container +{ + + // {{{ properties + + /** + * Options for the class + * @var array + */ + var $options = array(); + + // }}} + // {{{ Auth_Container_IMAP() [constructor] + + /** + * Constructor of the container class + * + * @param $params associative array with host, port, baseDSN, checkServer + * and userattr key + * @return object Returns an error object if something went wrong + * @todo Use PEAR Net_IMAP if IMAP extension not loaded + */ + function Auth_Container_IMAP($params) + { + if (!extension_loaded('imap')) { + return PEAR::raiseError('Cannot use IMAP authentication, ' + .'IMAP extension not loaded!', 41, PEAR_ERROR_DIE); + } + $this->_setDefaults(); + + // set parameters (if any) + if (is_array($params)) { + $this->_parseOptions($params); + } + + if ($this->options['checkServer']) { + $this->_checkServer($this->options['timeout']); + } + return true; + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + */ + function _setDefaults() + { + $this->options['host'] = 'localhost'; + $this->options['port'] = 143; + $this->options['baseDSN'] = ''; + $this->options['checkServer'] = true; + $this->options['timeout'] = 20; + } + + // }}} + // {{{ _checkServer() + + /** + * Check if the given server and port are reachable + * + * @access private + */ + function _checkServer() { + $this->log('Auth_Container_IMAP::_checkServer() called.', AUTH_LOG_DEBUG); + $fp = @fsockopen ($this->options['host'], $this->options['port'], + $errno, $errstr, $this->options['timeout']); + if (is_resource($fp)) { + @fclose($fp); + } else { + $message = "Error connecting to IMAP server " + . $this->options['host'] + . ":" . $this->options['port']; + return PEAR::raiseError($message, 41); + } + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + $this->options[$key] = $value; + } + } + + // }}} + // {{{ fetchData() + + /** + * Try to open a IMAP stream using $username / $password + * + * @param string Username + * @param string Password + * @return boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_IMAP::fetchData() called.', AUTH_LOG_DEBUG); + $dsn = '{'.$this->options['host'].':'.$this->options['port'].$this->options['baseDSN'].'}'; + $conn = @imap_open ($dsn, $username, $password, OP_HALFOPEN); + if (is_resource($conn)) { + $this->log('Successfully connected to IMAP server.', AUTH_LOG_DEBUG); + $this->activeUser = $username; + @imap_close($conn); + return true; + } else { + $this->log('Connection to IMAP server failed.', AUTH_LOG_DEBUG); + $this->activeUser = ''; + return false; + } + } + + // }}} + +} +?> diff --git a/Auth/Container/KADM5.php b/Auth/Container/KADM5.php new file mode 100644 index 0000000..7b8fe7d --- /dev/null +++ b/Auth/Container/KADM5.php @@ -0,0 +1,171 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: KADM5.php,v 1.6 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.4.0 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR for error handling + */ +require_once 'PEAR.php'; + +/** + * Storage driver for Authentication on a Kerberos V server. + * + * Available options: + * hostname: The hostname of the kerberos server + * realm: The Kerberos V realm + * timeout: The timeout for checking the server + * checkServer: Set to true to check if the server is running when + * constructing the object + * + * @category Authentication + * @package Auth + * @author Andrew Teixeira + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.6 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.4.0 + */ +class Auth_Container_KADM5 extends Auth_Container { + + // {{{ properties + + /** + * Options for the class + * @var string + */ + var $options = array(); + + // }}} + // {{{ Auth_Container_KADM5() + + /** + * Constructor of the container class + * + * $options can have these keys: + * 'hostname' The hostname of the kerberos server + * 'realm' The Kerberos V realm + * 'timeout' The timeout for checking the server + * 'checkServer' Set to true to check if the server is running when + * constructing the object + * + * @param $options associative array + * @return object Returns an error object if something went wrong + */ + function Auth_Container_KADM5($options) { + if (!extension_loaded('kadm5')) { + return PEAR::raiseError("Cannot use Kerberos V authentication, KADM5 extension not loaded!", 41, PEAR_ERROR_DIE); + } + + $this->_setDefaults(); + + if (isset($options['hostname'])) { + $this->options['hostname'] = $options['hostname']; + } + if (isset($options['realm'])) { + $this->options['realm'] = $options['realm']; + } + if (isset($options['timeout'])) { + $this->options['timeout'] = $options['timeout']; + } + if (isset($options['checkServer'])) { + $this->options['checkServer'] = $options['checkServer']; + } + + if ($this->options['checkServer']) { + $this->_checkServer(); + } + } + + // }}} + // {{{ fetchData() + + /** + * Try to login to the KADM5 server + * + * @param string Username + * @param string Password + * @return boolean + */ + function fetchData($username, $password) { + $this->log('Auth_Container_KADM5::fetchData() called.', AUTH_LOG_DEBUG); + if ( ($username == NULL) || ($password == NULL) ) { + return false; + } + + $server = $this->options['hostname']; + $realm = $this->options['realm']; + $check = @kadm5_init_with_password($server, $realm, $username, $password); + + if ($check == false) { + return false; + } else { + return true; + } + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + */ + function _setDefaults() { + $this->options['hostname'] = 'localhost'; + $this->options['realm'] = NULL; + $this->options['timeout'] = 10; + $this->options['checkServer'] = false; + } + + // }}} + // {{{ _checkServer() + + /** + * Check if the given server and port are reachable + * + * @access private + */ + function _checkServer() { + $fp = @fsockopen ($this->options['hostname'], 88, $errno, $errstr, $this->options['timeout']); + if (is_resource($fp)) { + @fclose($fp); + } else { + $message = "Error connecting to Kerberos V server " + .$this->options['hostname'].":".$this->options['port']; + return PEAR::raiseError($message, 41, PEAR_ERROR_DIE); + } + } + + // }}} + +} + +?> diff --git a/Auth/Container/LDAP.php b/Auth/Container/LDAP.php new file mode 100644 index 0000000..fe2817a --- /dev/null +++ b/Auth/Container/LDAP.php @@ -0,0 +1,766 @@ + + * @author Adam Ashley + * @author Hugues Peeters + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: LDAP.php,v 1.43 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from LDAP + * + * This class is heavily based on the DB and File containers. By default it + * connects to localhost:389 and searches for uid=$username with the scope + * "sub". If no search base is specified, it will try to determine it via + * the namingContexts attribute. It takes its parameters in a hash, connects + * to the ldap server, binds anonymously, searches for the user, and tries + * to bind as the user with the supplied password. When a group was set, it + * will look for group membership of the authenticated user. If all goes + * well the authentication was successful. + * + * Parameters: + * + * host: localhost (default), ldap.netsols.de or 127.0.0.1 + * port: 389 (default) or 636 or whereever your server runs + * url: ldap://localhost:389/ + * useful for ldaps://, works only with openldap2 ? + * it will be preferred over host and port + * version: LDAP version to use, ususally 2 (default) or 3, + * must be an integer! + * referrals: If set, determines whether the LDAP library automatically + * follows referrals returned by LDAP servers or not. Possible + * values are true (default) or false. + * binddn: If set, searching for user will be done after binding + * as this user, if not set the bind will be anonymous. + * This is reported to make the container work with MS + * Active Directory, but should work with any server that + * is configured this way. + * This has to be a complete dn for now (basedn and + * userdn will not be appended). + * bindpw: The password to use for binding with binddn + * basedn: the base dn of your server + * userdn: gets prepended to basedn when searching for user + * userscope: Scope for user searching: one, sub (default), or base + * userattr: the user attribute to search for (default: uid) + * userfilter: filter that will be added to the search filter + * this way: (&(userattr=username)(userfilter)) + * default: (objectClass=posixAccount) + * attributes: array of additional attributes to fetch from entry. + * these will added to auth data and can be retrieved via + * Auth::getAuthData(). An empty array will fetch all attributes, + * array('') will fetch no attributes at all (default) + * If you add 'dn' as a value to this array, the users DN that was + * used for binding will be added to auth data as well. + * attrformat: The returned format of the additional data defined in the + * 'attributes' option. Two formats are available. + * LDAP returns data formatted in a + * multidimensional array where each array starts with a + * 'count' element providing the number of attributes in the + * entry, or the number of values for attributes. When set + * to this format, the only way to retrieve data from the + * Auth object is by calling getAuthData('attributes'). + * AUTH returns data formatted in a + * structure more compliant with other Auth Containers, + * where each attribute element can be directly called by + * getAuthData() method from Auth. + * For compatibily with previous LDAP container versions, + * the default format is LDAP. + * groupdn: gets prepended to basedn when searching for group + * groupattr: the group attribute to search for (default: cn) + * groupfilter: filter that will be added to the search filter when + * searching for a group: + * (&(groupattr=group)(memberattr=username)(groupfilter)) + * default: (objectClass=groupOfUniqueNames) + * memberattr : the attribute of the group object where the user dn + * may be found (default: uniqueMember) + * memberisdn: whether the memberattr is the dn of the user (default) + * or the value of userattr (usually uid) + * group: the name of group to search for + * groupscope: Scope for group searching: one, sub (default), or base + * start_tls: enable/disable the use of START_TLS encrypted connection + * (default: false) + * debug: Enable/Disable debugging output (default: false) + * try_all: Whether to try all user accounts returned from the search + * or just the first one. (default: false) + * + * To use this storage container, you have to use the following syntax: + * + * 'localhost', + * 'port' => '389', + * 'version' => 3, + * 'basedn' => 'o=netsols,c=de', + * 'userattr' => 'uid' + * 'binddn' => 'cn=admin,o=netsols,c=de', + * 'bindpw' => 'password')); + * + * $a2 = new Auth('LDAP', array( + * 'url' => 'ldaps://ldap.netsols.de', + * 'basedn' => 'o=netsols,c=de', + * 'userscope' => 'one', + * 'userdn' => 'ou=People', + * 'groupdn' => 'ou=Groups', + * 'groupfilter' => '(objectClass=posixGroup)', + * 'memberattr' => 'memberUid', + * 'memberisdn' => false, + * 'group' => 'admin' + * )); + * + * $a3 = new Auth('LDAP', array( + * 'host' => 'ldap.netsols.de', + * 'port' => 389, + * 'version' => 3, + * 'referrals' => false, + * 'basedn' => 'dc=netsols,dc=de', + * 'binddn' => 'cn=Jan Wagner,cn=Users,dc=netsols,dc=de', + * 'bindpw' => 'password', + * 'userattr' => 'samAccountName', + * 'userfilter' => '(objectClass=user)', + * 'attributes' => array(''), + * 'group' => 'testing', + * 'groupattr' => 'samAccountName', + * 'groupfilter' => '(objectClass=group)', + * 'memberattr' => 'member', + * 'memberisdn' => true, + * 'groupdn' => 'cn=Users', + * 'groupscope' => 'one', + * 'debug' => true); + * + * The parameter values have to correspond + * to the ones for your LDAP server of course. + * + * When talking to a Microsoft ActiveDirectory server you have to + * use 'samaccountname' as the 'userattr' and follow special rules + * to translate the ActiveDirectory directory names into 'basedn'. + * The 'basedn' for the default 'Users' folder on an ActiveDirectory + * server for the ActiveDirectory Domain (which is not related to + * its DNS name) "win2000.example.org" would be: + * "CN=Users, DC=win2000, DC=example, DC=org' + * where every component of the domain name becomes a DC attribute + * of its own. If you want to use a custom users folder you have to + * replace "CN=Users" with a sequence of "OU" attributes that specify + * the path to your custom folder in reverse order. + * So the ActiveDirectory folder + * "win2000.example.org\Custom\Accounts" + * would become + * "OU=Accounts, OU=Custom, DC=win2000, DC=example, DC=org' + * + * It seems that binding anonymously to an Active Directory + * is not allowed, so you have to set binddn and bindpw for + * user searching. + * + * LDAP Referrals need to be set to false for AD to work sometimes. + * + * Example a3 shows a full blown and tested example for connection to + * Windows 2000 Active Directory with group mebership checking + * + * Note also that if you want an encrypted connection to an MS LDAP + * server, then, on your webserver, you must specify + * TLS_REQCERT never + * in /etc/ldap/ldap.conf or in the webserver user's ~/.ldaprc (which + * may or may not be read depending on your configuration). + * + * + * @category Authentication + * @package Auth + * @author Jan Wagner + * @author Adam Ashley + * @author Hugues Peeters + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.43 $ + * @link http://pear.php.net/package/Auth + */ +class Auth_Container_LDAP extends Auth_Container +{ + + // {{{ properties + + /** + * Options for the class + * @var array + */ + var $options = array(); + + /** + * Connection ID of LDAP Link + * @var string + */ + var $conn_id = false; + + // }}} + + // {{{ Auth_Container_LDAP() [constructor] + + /** + * Constructor of the container class + * + * @param $params, associative hash with host,port,basedn and userattr key + * @return object Returns an error object if something went wrong + */ + function Auth_Container_LDAP($params) + { + if (false === extension_loaded('ldap')) { + return PEAR::raiseError('Auth_Container_LDAP: LDAP Extension not loaded', + 41, PEAR_ERROR_DIE); + } + + $this->_setDefaults(); + + if (is_array($params)) { + $this->_parseOptions($params); + } + } + + // }}} + // {{{ _prepare() + + /** + * Prepare LDAP connection + * + * This function checks if we have already opened a connection to + * the LDAP server. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a PEAR error object. + */ + function _prepare() + { + if (!$this->_isValidLink()) { + $res = $this->_connect(); + if (PEAR::isError($res)) { + return $res; + } + } + return true; + } + + // }}} + // {{{ _connect() + + /** + * Connect to the LDAP server using the global options + * + * @access private + * @return object Returns a PEAR error object if an error occurs. + */ + function _connect() + { + $this->log('Auth_Container_LDAP::_connect() called.', AUTH_LOG_DEBUG); + // connect + if (isset($this->options['url']) && $this->options['url'] != '') { + $this->log('Connecting with URL', AUTH_LOG_DEBUG); + $conn_params = array($this->options['url']); + } else { + $this->log('Connecting with host:port', AUTH_LOG_DEBUG); + $conn_params = array($this->options['host'], $this->options['port']); + } + + if (($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) { + $this->log('Connection to server failed.', AUTH_LOG_DEBUG); + $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG); + return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41); + } + $this->log('Successfully connected to server', AUTH_LOG_DEBUG); + + // switch LDAP version + if (is_numeric($this->options['version']) && $this->options['version'] > 2) { + $this->log("Switching to LDAP version {$this->options['version']}", AUTH_LOG_DEBUG); + @ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $this->options['version']); + + // start TLS if available + if (isset($this->options['start_tls']) && $this->options['start_tls']) { + $this->log("Starting TLS session", AUTH_LOG_DEBUG); + if (@ldap_start_tls($this->conn_id) === false) { + $this->log('Could not start TLS session', AUTH_LOG_DEBUG); + $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG); + return PEAR::raiseError('Auth_Container_LDAP: Could not start tls.', 41); + } + } + } + + // switch LDAP referrals + if (is_bool($this->options['referrals'])) { + $this->log("Switching LDAP referrals to " . (($this->options['referrals']) ? 'true' : 'false'), AUTH_LOG_DEBUG); + if (@ldap_set_option($this->conn_id, LDAP_OPT_REFERRALS, $this->options['referrals']) === false) { + $this->log('Could not change LDAP referrals options', AUTH_LOG_DEBUG); + $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG); + } + } + + // bind with credentials or anonymously + if (strlen($this->options['binddn']) && strlen($this->options['bindpw'])) { + $this->log('Binding with credentials', AUTH_LOG_DEBUG); + $bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']); + } else { + $this->log('Binding anonymously', AUTH_LOG_DEBUG); + $bind_params = array($this->conn_id); + } + + // bind for searching + if ((@call_user_func_array('ldap_bind', $bind_params)) === false) { + $this->log('Bind failed', AUTH_LOG_DEBUG); + $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG); + $this->_disconnect(); + return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41); + } + $this->log('Binding was successful', AUTH_LOG_DEBUG); + + return true; + } + + // }}} + // {{{ _disconnect() + + /** + * Disconnects (unbinds) from ldap server + * + * @access private + */ + function _disconnect() + { + $this->log('Auth_Container_LDAP::_disconnect() called.', AUTH_LOG_DEBUG); + if ($this->_isValidLink()) { + $this->log('disconnecting from server'); + @ldap_unbind($this->conn_id); + } + } + + // }}} + // {{{ _getBaseDN() + + /** + * Tries to find Basedn via namingContext Attribute + * + * @access private + */ + function _getBaseDN() + { + $this->log('Auth_Container_LDAP::_getBaseDN() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if ($this->options['basedn'] == "" && $this->_isValidLink()) { + $this->log("basedn not set, searching via namingContexts.", AUTH_LOG_DEBUG); + + $result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts")); + + if (@ldap_count_entries($this->conn_id, $result_id) == 1) { + + $this->log("got result for namingContexts", AUTH_LOG_DEBUG); + + $entry_id = @ldap_first_entry($this->conn_id, $result_id); + $attrs = @ldap_get_attributes($this->conn_id, $entry_id); + $basedn = $attrs['namingContexts'][0]; + + if ($basedn != "") { + $this->log("result for namingContexts was $basedn", AUTH_LOG_DEBUG); + $this->options['basedn'] = $basedn; + } + } + @ldap_free_result($result_id); + } + + // if base ist still not set, raise error + if ($this->options['basedn'] == "") { + return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41); + } + return true; + } + + // }}} + // {{{ _isValidLink() + + /** + * determines whether there is a valid ldap conenction or not + * + * @accessd private + * @return boolean + */ + function _isValidLink() + { + if (is_resource($this->conn_id)) { + if (get_resource_type($this->conn_id) == 'ldap link') { + return true; + } + } + return false; + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + */ + function _setDefaults() + { + $this->options['url'] = ''; + $this->options['host'] = 'localhost'; + $this->options['port'] = '389'; + $this->options['version'] = 2; + $this->options['referrals'] = true; + $this->options['binddn'] = ''; + $this->options['bindpw'] = ''; + $this->options['basedn'] = ''; + $this->options['userdn'] = ''; + $this->options['userscope'] = 'sub'; + $this->options['userattr'] = 'uid'; + $this->options['userfilter'] = '(objectClass=posixAccount)'; + $this->options['attributes'] = array(''); // no attributes + $this->options['attrformat'] = 'AUTH'; // returns attribute like other Auth containers + $this->options['group'] = ''; + $this->options['groupdn'] = ''; + $this->options['groupscope'] = 'sub'; + $this->options['groupattr'] = 'cn'; + $this->options['groupfilter'] = '(objectClass=groupOfUniqueNames)'; + $this->options['memberattr'] = 'uniqueMember'; + $this->options['memberisdn'] = true; + $this->options['start_tls'] = false; + $this->options['debug'] = false; + $this->options['try_all'] = false; // Try all user ids returned not just the first one + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + $array = $this->_setV12OptionsToV13($array); + + foreach ($array as $key => $value) { + if (array_key_exists($key, $this->options)) { + if ($key == 'attributes') { + if (is_array($value)) { + $this->options[$key] = $value; + } else { + $this->options[$key] = explode(',', $value); + } + } else { + $this->options[$key] = $value; + } + } + } + } + + // }}} + // {{{ _setV12OptionsToV13() + + /** + * Adapt deprecated options from Auth 1.2 LDAP to Auth 1.3 LDAP + * + * @author Hugues Peeters + * @access private + * @param array + * @return array + */ + function _setV12OptionsToV13($array) + { + if (isset($array['useroc'])) + $array['userfilter'] = "(objectClass=".$array['useroc'].")"; + if (isset($array['groupoc'])) + $array['groupfilter'] = "(objectClass=".$array['groupoc'].")"; + if (isset($array['scope'])) + $array['userscope'] = $array['scope']; + + return $array; + } + + // }}} + // {{{ _scope2function() + + /** + * Get search function for scope + * + * @param string scope + * @return string ldap search function + */ + function _scope2function($scope) + { + switch($scope) { + case 'one': + $function = 'ldap_list'; + break; + case 'base': + $function = 'ldap_read'; + break; + default: + $function = 'ldap_search'; + break; + } + return $function; + } + + // }}} + // {{{ fetchData() + + /** + * Fetch data from LDAP server + * + * Searches the LDAP server for the given username/password + * combination. Escapes all LDAP meta characters in username + * before performing the query. + * + * @param string Username + * @param string Password + * @return boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_LDAP::fetchData() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $err = $this->_getBaseDN(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // UTF8 Encode username for LDAPv3 + if (@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver == 3) { + $this->log('UTF8 encoding username for LDAPv3', AUTH_LOG_DEBUG); + $username = utf8_encode($username); + } + + // make search filter + $filter = sprintf('(&(%s=%s)%s)', + $this->options['userattr'], + $this->_quoteFilterString($username), + $this->options['userfilter']); + + // make search base dn + $search_basedn = $this->options['userdn']; + if ($search_basedn != '' && substr($search_basedn, -1) != ',') { + $search_basedn .= ','; + } + $search_basedn .= $this->options['basedn']; + + // attributes + $searchAttributes = $this->options['attributes']; + + // make functions params array + $func_params = array($this->conn_id, $search_basedn, $filter, $searchAttributes); + + // search function to use + $func_name = $this->_scope2function($this->options['userscope']); + + $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG); + + // search + if (($result_id = @call_user_func_array($func_name, $func_params)) === false) { + $this->log('User not found', AUTH_LOG_DEBUG); + } elseif (@ldap_count_entries($this->conn_id, $result_id) >= 1) { // did we get some possible results? + + $this->log('User(s) found', AUTH_LOG_DEBUG); + + $first = true; + $entry_id = null; + + do { + + // then get the user dn + if ($first) { + $entry_id = @ldap_first_entry($this->conn_id, $result_id); + $first = false; + } else { + $entry_id = @ldap_next_entry($this->conn_id, $entry_id); + if ($entry_id === false) + break; + } + $user_dn = @ldap_get_dn($this->conn_id, $entry_id); + + // as the dn is not fetched as an attribute, we save it anyway + if (is_array($searchAttributes) && in_array('dn', $searchAttributes)) { + $this->log('Saving DN to AuthData', AUTH_LOG_DEBUG); + $this->_auth_obj->setAuthData('dn', $user_dn); + } + + // fetch attributes + if ($attributes = @ldap_get_attributes($this->conn_id, $entry_id)) { + + if (is_array($attributes) && isset($attributes['count']) && + $attributes['count'] > 0) { + + // ldap_get_attributes() returns a specific multi dimensional array + // format containing all the attributes and where each array starts + // with a 'count' element providing the number of attributes in the + // entry, or the number of values for attribute. For compatibility + // reasons, it remains the default format returned by LDAP container + // setAuthData(). + // The code below optionally returns attributes in another format, + // more compliant with other Auth containers, where each attribute + // element are directly set in the 'authData' list. This option is + // enabled by setting 'attrformat' to + // 'AUTH' in the 'options' array. + // eg. $this->options['attrformat'] = 'AUTH' + + if ( strtoupper($this->options['attrformat']) == 'AUTH' ) { + $this->log('Saving attributes to Auth data in AUTH format', AUTH_LOG_DEBUG); + unset ($attributes['count']); + foreach ($attributes as $attributeName => $attributeValue ) { + if (is_int($attributeName)) continue; + if (is_array($attributeValue) && isset($attributeValue['count'])) { + unset ($attributeValue['count']); + } + if (count($attributeValue)<=1) $attributeValue = $attributeValue[0]; + $this->log('Storing additional field: '.$attributeName, AUTH_LOG_DEBUG); + $this->_auth_obj->setAuthData($attributeName, $attributeValue); + } + } + else + { + $this->log('Saving attributes to Auth data in LDAP format', AUTH_LOG_DEBUG); + $this->_auth_obj->setAuthData('attributes', $attributes); + } + } + } + @ldap_free_result($result_id); + + // need to catch an empty password as openldap seems to return TRUE + // if anonymous binding is allowed + if ($password != "") { + $this->log("Bind as $user_dn", AUTH_LOG_DEBUG); + + // try binding as this user with the supplied password + if (@ldap_bind($this->conn_id, $user_dn, $password)) { + $this->log('Bind successful', AUTH_LOG_DEBUG); + + // check group if appropiate + if (strlen($this->options['group'])) { + // decide whether memberattr value is a dn or the username + $this->log('Checking group membership', AUTH_LOG_DEBUG); + $return = $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username); + $this->_disconnect(); + return $return; + } else { + $this->log('Authenticated', AUTH_LOG_DEBUG); + $this->_disconnect(); + return true; // user authenticated + } // checkGroup + } // bind + } // non-empty password + } while ($this->options['try_all'] == true); // interate through entries + } // get results + // default + $this->log('NOT authenticated!', AUTH_LOG_DEBUG); + $this->_disconnect(); + return false; + } + + // }}} + // {{{ checkGroup() + + /** + * Validate group membership + * + * Searches the LDAP server for group membership of the + * supplied username. Quotes all LDAP filter meta characters in + * the user name before querying the LDAP server. + * + * @param string Distinguished Name of the authenticated User + * @return boolean + */ + function checkGroup($user) + { + $this->log('Auth_Container_LDAP::checkGroup() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // make filter + $filter = sprintf('(&(%s=%s)(%s=%s)%s)', + $this->options['groupattr'], + $this->options['group'], + $this->options['memberattr'], + $this->_quoteFilterString($user), + $this->options['groupfilter']); + + // make search base dn + $search_basedn = $this->options['groupdn']; + if ($search_basedn != '' && substr($search_basedn, -1) != ',') { + $search_basedn .= ','; + } + $search_basedn .= $this->options['basedn']; + + $func_params = array($this->conn_id, $search_basedn, $filter, + array($this->options['memberattr'])); + $func_name = $this->_scope2function($this->options['groupscope']); + + $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG); + + // search + if (($result_id = @call_user_func_array($func_name, $func_params)) != false) { + if (@ldap_count_entries($this->conn_id, $result_id) == 1) { + @ldap_free_result($result_id); + $this->log('User is member of group', AUTH_LOG_DEBUG); + return true; + } + } + // default + $this->log('User is NOT member of group', AUTH_LOG_DEBUG); + return false; + } + + // }}} + // {{{ _quoteFilterString() + + /** + * Escapes LDAP filter special characters as defined in RFC 2254. + * + * @access private + * @param string Filter String + */ + function _quoteFilterString($filter_str) + { + $metas = array( '\\', '*', '(', ')', "\x00"); + $quoted_metas = array('\\\\', '\*', '\(', '\)', "\\\x00"); + return str_replace($metas, $quoted_metas, $filter_str); + } + + // }}} + +} + +?> diff --git a/Auth/Container/MDB.php b/Auth/Container/MDB.php new file mode 100644 index 0000000..f8f7ecc --- /dev/null +++ b/Auth/Container/MDB.php @@ -0,0 +1,618 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: MDB.php,v 1.35 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.3 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR MDB package + */ +require_once 'MDB.php'; + +/** + * Storage driver for fetching login data from a database + * + * This storage driver can use all databases which are supported + * by the PEAR MDB abstraction layer to fetch login data. + * + * @category Authentication + * @package Auth + * @author Lorenzo Alberton + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.35 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.3 + */ +class Auth_Container_MDB extends Auth_Container +{ + + // {{{ properties + + /** + * Additional options for the storage container + * @var array + */ + var $options = array(); + + /** + * MDB object + * @var object + */ + var $db = null; + var $dsn = ''; + + /** + * User that is currently selected from the DB. + * @var string + */ + var $activeUser = ''; + + // }}} + // {{{ Auth_Container_MDB() [constructor] + + /** + * Constructor of the container class + * + * Initate connection to the database via PEAR::MDB + * + * @param string Connection data or MDB object + * @return object Returns an error object if something went wrong + */ + function Auth_Container_MDB($dsn) + { + $this->_setDefaults(); + + if (is_array($dsn)) { + $this->_parseOptions($dsn); + if (empty($this->options['dsn'])) { + PEAR::raiseError('No connection parameters specified!'); + } + } else { + $this->options['dsn'] = $dsn; + } + } + + // }}} + // {{{ _connect() + + /** + * Connect to database by using the given DSN string + * + * @access private + * @param mixed DSN string | array | mdb object + * @return mixed Object on error, otherwise bool + */ + function _connect($dsn) + { + $this->log('Auth_Container_MDB::_connect() called.', AUTH_LOG_DEBUG); + if (is_string($dsn) || is_array($dsn)) { + $this->db =& MDB::connect($dsn, $this->options['db_options']); + } elseif (is_subclass_of($dsn, 'mdb_common')) { + $this->db = $dsn; + } elseif (is_object($dsn) && MDB::isError($dsn)) { + return PEAR::raiseError($dsn->getMessage(), $dsn->code); + } else { + return PEAR::raiseError('The given dsn was not valid in file ' . __FILE__ . ' at line ' . __LINE__, + 41, + PEAR_ERROR_RETURN, + null, + null + ); + + } + + if (MDB::isError($this->db) || PEAR::isError($this->db)) { + return PEAR::raiseError($this->db->getMessage(), $this->db->code); + } + + if ($this->options['auto_quote']) { + $this->options['final_table'] = $this->db->quoteIdentifier($this->options['table']); + $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol']); + $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol']); + } else { + $this->options['final_table'] = $this->options['table']; + $this->options['final_usernamecol'] = $this->options['usernamecol']; + $this->options['final_passwordcol'] = $this->options['passwordcol']; + } + + return true; + } + + // }}} + // {{{ _prepare() + + /** + * Prepare database connection + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a MDB error object. + */ + function _prepare() + { + if (is_subclass_of($this->db, 'mdb_common')) { + return true; + } + return $this->_connect($this->options['dsn']); + } + + // }}} + // {{{ query() + + /** + * Prepare query to the database + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * After that the query is passed to the database. + * + * @access public + * @param string Query string + * @return mixed a MDB_result object or MDB_OK on success, a MDB + * or PEAR error on failure + */ + function query($query) + { + $this->log('Auth_Container_MDB::query() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return $err; + } + return $this->db->query($query); + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->options['table'] = 'auth'; + $this->options['usernamecol'] = 'username'; + $this->options['passwordcol'] = 'password'; + $this->options['dsn'] = ''; + $this->options['db_fields'] = ''; + $this->options['cryptType'] = 'md5'; + $this->options['db_options'] = array(); + $this->options['db_where'] = ''; + $this->options['auto_quote'] = true; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + } + + // }}} + // {{{ _quoteDBFields() + + /** + * Quote the db_fields option to avoid the possibility of SQL injection. + * + * @access private + * @return string A properly quoted string that can be concatenated into a + * SELECT clause. + */ + function _quoteDBFields() + { + if (isset($this->options['db_fields'])) { + if (is_array($this->options['db_fields'])) { + if ($this->options['auto_quote']) { + $fields = array(); + foreach ($this->options['db_fields'] as $field) { + $fields[] = $this->db->quoteIdentifier($field); + } + return implode(', ', $fields); + } else { + return implode(', ', $this->options['db_fields']); + } + } else { + if (strlen($this->options['db_fields']) > 0) { + if ($this->options['auto_quote']) { + return $this->db->quoteIdentifier($this->options['db_fields']); + } else { + return $this->options['db_fields']; + } + } + } + } + + return ''; + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from database + * + * This function uses the given username to fetch + * the corresponding login data from the database + * table. If an account that matches the passed username + * and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @param boolean If true password is secured using a md5 hash + * the frontend and auth are responsible for making sure the container supports + * challenge response password authentication + * @return mixed Error object or boolean + */ + function fetchData($username, $password, $isChallengeResponse=false) + { + $this->log('Auth_Container_MDB::fetchData() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + //Check if db_fields contains a *, if so assume all columns are selected + if (is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = '*'; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = sprintf("SELECT %s FROM %s WHERE %s = %s", + $sql_from, + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->db->getTextValue($username) + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->getRow($query, null, null, null, MDB_FETCHMODE_ASSOC); + + if (MDB::isError($res) || PEAR::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } + if (!is_array($res)) { + $this->activeUser = ''; + return false; + } + + // Perform trimming here before the hashing + $password = trim($password, "\r\n"); + $res[$this->options['passwordcol']] = trim($res[$this->options['passwordcol']], "\r\n"); + + // If using Challenge Response md5 the pass with the secret + if ($isChallengeResponse) { + $res[$this->options['passwordcol']] = + md5($res[$this->options['passwordcol']].$this->_auth_obj->session['loginchallenege']); + // UGLY cannot avoid without modifying verifyPassword + if ($this->options['cryptType'] == 'md5') { + $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']]); + } + } + + if ($this->verifyPassword($password, + $res[$this->options['passwordcol']], + $this->options['cryptType'])) { + // Store additional field values in the session + foreach ($res as $key => $value) { + if ($key == $this->options['passwordcol'] || + $key == $this->options['usernamecol']) { + continue; + } + + $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG); + // Use reference to the auth object if exists + // This is because the auth session variable can change so a static + // call to setAuthData does not make sense + $this->_auth_obj->setAuthData($key, $value); + } + return true; + } + + $this->activeUser = $res[$this->options['usernamecol']]; + return false; + } + + // }}} + // {{{ listUsers() + + /** + * Returns a list of users from the container + * + * @return mixed array|PEAR_Error + * @access public + */ + function listUsers() + { + $this->log('Auth_Container_MDB::listUsers() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $retVal = array(); + + //Check if db_fields contains a *, if so assume all columns are selected + if ( is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = '*'; + } else { + $sql_from = $this->options['final_usernamecol'] + .', '.$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = sprintf('SELECT %s FROM %s', + $sql_from, + $this->options['final_table'] + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " WHERE ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->getAll($query, null, null, null, MDB_FETCHMODE_ASSOC); + + if (MDB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + foreach ($res as $user) { + $user['username'] = $user[$this->options['usernamecol']]; + $retVal[] = $user; + } + } + $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG); + return $retVal; + } + + // }}} + // {{{ addUser() + + /** + * Add user to the storage container + * + * @access public + * @param string Username + * @param string Password + * @param mixed Additional information that are stored in the DB + * + * @return mixed True on success, otherwise error object + */ + function addUser($username, $password, $additional = "") + { + $this->log('Auth_Container_MDB::addUser() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + $additional_key = ''; + $additional_value = ''; + + if (is_array($additional)) { + foreach ($additional as $key => $value) { + if ($this->options['auto_quote']) { + $additional_key .= ', ' . $this->db->quoteIdentifier($key); + } else { + $additional_key .= ', ' . $key; + } + $additional_value .= ', ' . $this->db->getTextValue($value); + } + } + + $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES (%s, %s%s)", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->options['final_passwordcol'], + $additional_key, + $this->db->getTextValue($username), + $this->db->getTextValue($password), + $additional_value + ); + + $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @access public + * @param string Username + * + * @return mixed True on success, otherwise error object + */ + function removeUser($username) + { + $this->log('Auth_Container_MDB::removeUser() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $query = sprintf("DELETE FROM %s WHERE %s = %s", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->db->getTextValue($username) + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password (plain text) + */ + function changePassword($username, $password) + { + $this->log('Auth_Container_MDB::changePassword() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + $query = sprintf("UPDATE %s SET %s = %s WHERE %s = %s", + $this->options['final_table'], + $this->options['final_passwordcol'], + $this->db->getTextValue($password), + $this->options['final_usernamecol'], + $this->db->getTextValue($username) + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ supportsChallengeResponse() + + /** + * Determine if this container supports + * password authentication with challenge response + * + * @return bool + * @access public + */ + function supportsChallengeResponse() + { + return in_array($this->options['cryptType'], array('md5', 'none', '')); + } + + // }}} + // {{{ getCryptType() + + /** + * Returns the selected crypt type for this container + * + * @return string Function used to crypt the password + */ + function getCryptType() + { + return $this->options['cryptType']; + } + + // }}} + +} +?> diff --git a/Auth/Container/MDB2.php b/Auth/Container/MDB2.php new file mode 100644 index 0000000..0f4e6fa --- /dev/null +++ b/Auth/Container/MDB2.php @@ -0,0 +1,617 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: MDB2.php,v 1.22 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR MDB2 package + */ +require_once 'MDB2.php'; + +/** + * Storage driver for fetching login data from a database + * + * This storage driver can use all databases which are supported + * by the PEAR MDB2 abstraction layer to fetch login data. + * + * @category Authentication + * @package Auth + * @author Lorenzo Alberton + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.22 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Container_MDB2 extends Auth_Container +{ + + // {{{ properties + + /** + * Additional options for the storage container + * @var array + */ + var $options = array(); + + /** + * MDB object + * @var object + */ + var $db = null; + var $dsn = ''; + + /** + * User that is currently selected from the DB. + * @var string + */ + var $activeUser = ''; + + // }}} + // {{{ Auth_Container_MDB2() [constructor] + + /** + * Constructor of the container class + * + * Initate connection to the database via PEAR::MDB2 + * + * @param string Connection data or MDB2 object + * @return object Returns an error object if something went wrong + */ + function Auth_Container_MDB2($dsn) + { + $this->_setDefaults(); + + if (is_array($dsn)) { + $this->_parseOptions($dsn); + if (empty($this->options['dsn'])) { + PEAR::raiseError('No connection parameters specified!'); + } + } else { + $this->options['dsn'] = $dsn; + } + } + + // }}} + // {{{ _connect() + + /** + * Connect to database by using the given DSN string + * + * @access private + * @param mixed DSN string | array | mdb object + * @return mixed Object on error, otherwise bool + */ + function _connect($dsn) + { + $this->log('Auth_Container_MDB2::_connect() called.', AUTH_LOG_DEBUG); + if (is_string($dsn) || is_array($dsn)) { + $this->db =& MDB2::connect($dsn, $this->options['db_options']); + } elseif (is_subclass_of($dsn, 'MDB2_Driver_Common')) { + $this->db = $dsn; + } elseif (is_object($dsn) && MDB2::isError($dsn)) { + return PEAR::raiseError($dsn->getMessage(), $dsn->code); + } else { + return PEAR::raiseError('The given dsn was not valid in file ' . __FILE__ . ' at line ' . __LINE__, + 41, + PEAR_ERROR_RETURN, + null, + null + ); + + } + + if (MDB2::isError($this->db) || PEAR::isError($this->db)) { + return PEAR::raiseError($this->db->getMessage(), $this->db->code); + } + + if ($this->options['auto_quote']) { + $this->options['final_table'] = $this->db->quoteIdentifier($this->options['table'], true); + $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol'], true); + $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol'], true); + } else { + $this->options['final_table'] = $this->options['table']; + $this->options['final_usernamecol'] = $this->options['usernamecol']; + $this->options['final_passwordcol'] = $this->options['passwordcol']; + } + + return true; + } + + // }}} + // {{{ _prepare() + + /** + * Prepare database connection + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a MDB error object. + */ + function _prepare() + { + if (is_subclass_of($this->db, 'MDB2_Driver_Common')) { + return true; + } + return $this->_connect($this->options['dsn']); + } + + // }}} + // {{{ query() + + /** + * Prepare query to the database + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * After that the query is passed to the database. + * + * @access public + * @param string Query string + * @return mixed a MDB_result object or MDB_OK on success, a MDB + * or PEAR error on failure + */ + function query($query) + { + $this->log('Auth_Container_MDB2::query() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return $err; + } + return $this->db->exec($query); + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->options['table'] = 'auth'; + $this->options['usernamecol'] = 'username'; + $this->options['passwordcol'] = 'password'; + $this->options['dsn'] = ''; + $this->options['db_fields'] = ''; + $this->options['cryptType'] = 'md5'; + $this->options['db_options'] = array(); + $this->options['db_where'] = ''; + $this->options['auto_quote'] = true; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + } + + // }}} + // {{{ _quoteDBFields() + + /** + * Quote the db_fields option to avoid the possibility of SQL injection. + * + * @access private + * @return string A properly quoted string that can be concatenated into a + * SELECT clause. + */ + function _quoteDBFields() + { + if (isset($this->options['db_fields'])) { + if (is_array($this->options['db_fields'])) { + if ($this->options['auto_quote']) { + $fields = array(); + foreach ($this->options['db_fields'] as $field) { + $fields[] = $this->db->quoteIdentifier($field, true); + } + return implode(', ', $fields); + } else { + return implode(', ', $this->options['db_fields']); + } + } else { + if (strlen($this->options['db_fields']) > 0) { + if ($this->options['auto_quote']) { + return $this->db->quoteIdentifier($this->options['db_fields'], true); + } else { + return $this->options['db_fields']; + } + } + } + } + + return ''; + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from database + * + * This function uses the given username to fetch + * the corresponding login data from the database + * table. If an account that matches the passed username + * and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @param boolean If true password is secured using a md5 hash + * the frontend and auth are responsible for making sure the container supports + * challenge response password authentication + * @return mixed Error object or boolean + */ + function fetchData($username, $password, $isChallengeResponse=false) + { + $this->log('Auth_Container_MDB2::fetchData() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + //Check if db_fields contains a *, if so assume all columns are selected + if (is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = '*'; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + $query = sprintf("SELECT %s FROM %s WHERE %s = %s", + $sql_from, + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->db->quote($username, 'text') + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB2: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->queryRow($query, null, MDB2_FETCHMODE_ASSOC); + if (MDB2::isError($res) || PEAR::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } + if (!is_array($res)) { + $this->activeUser = ''; + return false; + } + + // Perform trimming here before the hashing + $password = trim($password, "\r\n"); + $res[$this->options['passwordcol']] = trim($res[$this->options['passwordcol']], "\r\n"); + // If using Challenge Response md5 the pass with the secret + if ($isChallengeResponse) { + $res[$this->options['passwordcol']] = + md5($res[$this->options['passwordcol']].$this->_auth_obj->session['loginchallenege']); + // UGLY cannot avoid without modifying verifyPassword + if ($this->options['cryptType'] == 'md5') { + $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']]); + } + } + if ($this->verifyPassword($password, + $res[$this->options['passwordcol']], + $this->options['cryptType'])) { + // Store additional field values in the session + foreach ($res as $key => $value) { + if ($key == $this->options['passwordcol'] || + $key == $this->options['usernamecol']) { + continue; + } + + $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG); + + // Use reference to the auth object if exists + // This is because the auth session variable can change so a static call to setAuthData does not make sense + $this->_auth_obj->setAuthData($key, $value); + } + return true; + } + + $this->activeUser = $res[$this->options['usernamecol']]; + return false; + } + + // }}} + // {{{ listUsers() + + /** + * Returns a list of users from the container + * + * @return mixed array|PEAR_Error + * @access public + */ + function listUsers() + { + $this->log('Auth_Container_MDB2::listUsers() called.', AUTH_LOG_DEBUG); + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $retVal = array(); + + //Check if db_fields contains a *, if so assume all columns are selected + if ( is_string($this->options['db_fields']) + && strstr($this->options['db_fields'], '*')) { + $sql_from = '*'; + } else { + $sql_from = $this->options['final_usernamecol']. + ", ".$this->options['final_passwordcol']; + + if (strlen($fields = $this->_quoteDBFields()) > 0) { + $sql_from .= ', '.$fields; + } + } + + $query = sprintf('SELECT %s FROM %s', + $sql_from, + $this->options['final_table'] + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " WHERE ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB2: '.$query, AUTH_LOG_DEBUG); + + $res = $this->db->queryAll($query, null, MDB2_FETCHMODE_ASSOC); + if (MDB2::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + foreach ($res as $user) { + $user['username'] = $user[$this->options['usernamecol']]; + $retVal[] = $user; + } + } + $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG); + return $retVal; + } + + // }}} + // {{{ addUser() + + /** + * Add user to the storage container + * + * @access public + * @param string Username + * @param string Password + * @param mixed Additional information that are stored in the DB + * + * @return mixed True on success, otherwise error object + */ + function addUser($username, $password, $additional = "") + { + $this->log('Auth_Container_MDB2::addUser() called.', AUTH_LOG_DEBUG); + + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + $additional_key = ''; + $additional_value = ''; + + if (is_array($additional)) { + foreach ($additional as $key => $value) { + if ($this->options['auto_quote']) { + $additional_key .= ', ' . $this->db->quoteIdentifier($key, true); + } else { + $additional_key .= ', ' . $key; + } + $additional_value .= ', ' . $this->db->quote($value, 'text'); + } + } + + $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES (%s, %s%s)", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->options['final_passwordcol'], + $additional_key, + $this->db->quote($username, 'text'), + $this->db->quote($password, 'text'), + $additional_value + ); + + $this->log('Running SQL against MDB2: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB2::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @access public + * @param string Username + * + * @return mixed True on success, otherwise error object + */ + function removeUser($username) + { + $this->log('Auth_Container_MDB2::removeUser() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $query = sprintf("DELETE FROM %s WHERE %s = %s", + $this->options['final_table'], + $this->options['final_usernamecol'], + $this->db->quote($username, 'text') + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB2: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB2::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password (plain text) + */ + function changePassword($username, $password) + { + $this->log('Auth_Container_MDB2::changePassword() called.', AUTH_LOG_DEBUG); + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { + $cryptFunction = 'strval'; + } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $password = $cryptFunction($password); + + $query = sprintf("UPDATE %s SET %s = %s WHERE %s = %s", + $this->options['final_table'], + $this->options['final_passwordcol'], + $this->db->quote($password, 'text'), + $this->options['final_usernamecol'], + $this->db->quote($username, 'text') + ); + + // check if there is an optional parameter db_where + if ($this->options['db_where'] != '') { + // there is one, so add it to the query + $query .= " AND ".$this->options['db_where']; + } + + $this->log('Running SQL against MDB2: '.$query, AUTH_LOG_DEBUG); + + $res = $this->query($query); + + if (MDB2::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->code); + } + return true; + } + + // }}} + // {{{ supportsChallengeResponse() + + /** + * Determine if this container supports + * password authentication with challenge response + * + * @return bool + * @access public + */ + function supportsChallengeResponse() + { + return in_array($this->options['cryptType'], array('md5', 'none', '')); + } + + // }}} + // {{{ getCryptType() + + /** + * Returns the selected crypt type for this container + * + * @return string Function used to crypt the password + */ + function getCryptType() + { + return $this->options['cryptType']; + } + + // }}} + +} +?> diff --git a/Auth/Container/Multiple.php b/Auth/Container/Multiple.php new file mode 100644 index 0000000..6b51b20 --- /dev/null +++ b/Auth/Container/Multiple.php @@ -0,0 +1,188 @@ + + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Multiple.php,v 1.4 2007/06/12 03:11:26 aashley Exp $ + * @since File available since Release 1.5.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for using multiple storage drivers in a fall through fashion + * + * This storage driver provides a mechanism for working through multiple + * storage drivers until either one allows successful login or the list is + * exhausted. + * + * This container takes an array of options of the following form: + * + * array( + * array( + * 'type' => , + * 'options' => , + * ), + * ); + * + * Full example: + * + * $options = array( + * array( + * 'type' => 'DB', + * 'options' => array( + * 'dsn' => "mysql://user:password@localhost/database", + * ), + * ), + * array( + * 'type' => 'Array', + * 'options' => array( + * 'cryptType' => 'md5', + * 'users' => array( + * 'admin' => md5('password'), + * ), + * ), + * ), + * ); + * + * $auth = new Auth('Multiple', $options); + * + * @category Authentication + * @package Auth + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.4 $ + * @since File available since Release 1.5.0 + */ + +class Auth_Container_Multiple extends Auth_Container { + + // {{{ properties + + /** + * The options for each container + * + * @var array $options + */ + var $options = array(); + + /** + * The instanciated containers + * + * @var array $containers + */ + var $containers = array(); + + // }}} + // {{{ Auth_Container_Multiple() + + /** + * Constructor for Array Container + * + * @param array $data Options for the container + * @return void + */ + function Auth_Container_Multiple($options) + { + if (!is_array($options)) { + PEAR::raiseError('The options for Auth_Container_Multiple must be an array'); + } + if (count($options) < 1) { + PEAR::raiseError('You must define at least one sub container to use in Auth_Container_Multiple'); + } + foreach ($options as $option) { + if (!isset($option['type'])) { + PEAR::raiseError('No type defined for sub container'); + } + } + $this->options = $options; + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from array + * + * This function uses the given username to fetch the corresponding + * login data from the array. If an account that matches the passed + * username and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @return boolean|PEAR_Error Error object or boolean + */ + function fetchData($user, $pass) + { + $this->log('Auth_Container_Multiple::fetchData() called.', AUTH_LOG_DEBUG); + + foreach ($this->options as $key => $options) { + + $this->log('Using Container '.$key.' of type '.$options['type'].'.', AUTH_LOG_DEBUG); + + if (isset($this->containers[$key]) && is_a($this->containers[$key], 'Auth_Container')) { + + $container = &$this->containers[$key]; + + } else { + + $this->containers[$key] = &$this->_auth_obj->_factory($options['type'], $options['options']); + $this->containers[$key]->_auth_obj = &$this->_auth_obj; + $container = &$this->containers[$key]; + + } + + $result = $container->fetchData($user, $pass); + + if (PEAR::isError($result)) { + + $this->log('Container '.$key.': '.$result->getMessage(), AUTH_LOG_ERR); + return $result; + + } elseif ($result == true) { + + $this->log('Container '.$key.': Authentication successful.', AUTH_LOG_DEBUG); + return true; + + } else { + + $this->log('Container '.$key.': Authentication failed.', AUTH_LOG_DEBUG); + + } + + } + + $this->log('Auth_Container_Multiple: All containers rejected user credentials.', AUTH_LOG_DEBUG); + + return false; + + } + + // }}} + +} + +?> diff --git a/Auth/Container/PEAR.php b/Auth/Container/PEAR.php new file mode 100644 index 0000000..cbf2005 --- /dev/null +++ b/Auth/Container/PEAR.php @@ -0,0 +1,115 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: PEAR.php,v 1.12 2007/07/02 05:09:43 aharvey Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Include PEAR HTTP_Client. + */ +require_once 'HTTP/Client.php'; +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; + +/** + * Storage driver for authenticating against PEAR website + * + * This driver provides a method for authenticating against the pear.php.net + * authentication system. + * + * @category Authentication + * @package Auth + * @author Yavor Shahpasov + * @author Adam Ashley + * @author Adam Harvey + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.12 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Container_Pear extends Auth_Container +{ + + // {{{ Auth_Container_Pear() [constructor] + + /** + * Constructor + * + * Currently does nothing + * + * @return void + */ + function Auth_Container_Pear() + { + + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from pear.php.net + * + * This function uses the given username and password to authenticate + * against the pear.php.net website + * + * @param string Username + * @param string Password + * @return mixed Error object or boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_PEAR::fetchData() called.', AUTH_LOG_DEBUG); + + $client = new HTTP_Client; + + $this->log('Auth_Container_PEAR::fetchData() getting salt.', AUTH_LOG_DEBUG); + $code = $client->get('https://pear.php.net/rest-login.php/getsalt'); + if ($code != 200) { + return PEAR::raiseError('Bad response to salt request.', $code); + } + $resp = $client->currentResponse(); + $salt = $resp['body']; + + $this->log('Auth_Container_PEAR::fetchData() calling validate.', AUTH_LOG_DEBUG); + $code = $client->post('https://pear.php.net/rest-login.php/validate', + array('username' => $username, + 'password' => md5($salt.md5($password)))); + if ($code != 200) { + return PEAR::raiseError('Bad response to validate request.', $code); + } + $resp = $client->currentResponse(); + + list($code, $message) = explode(' ', $resp['body'], 1); + if ($code != 8) { + return PEAR::raiseError($message, $code); + } + return true; + } + + // }}} + +} +?> diff --git a/Auth/Container/POP3.php b/Auth/Container/POP3.php new file mode 100644 index 0000000..444962a --- /dev/null +++ b/Auth/Container/POP3.php @@ -0,0 +1,145 @@ + + * @author Martin Jansen + * @author Mika Tuupola + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: POP3.php,v 1.12 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.0 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR package for error handling + */ +require_once 'PEAR.php'; +/** + * Include PEAR Net_POP3 package + */ +require_once 'Net/POP3.php'; + +/** + * Storage driver for Authentication on a POP3 server. + * + * @category Authentication + * @package Auth + * @author Martin Jansen + * @author Mika Tuupola + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.12 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.0 + */ +class Auth_Container_POP3 extends Auth_Container +{ + + // {{{ properties + + /** + * POP3 Server + * @var string + */ + var $server='localhost'; + + /** + * POP3 Server port + * @var string + */ + var $port='110'; + + /** + * POP3 Authentication method + * + * Prefered POP3 authentication method. Acceptable values: + * Boolean TRUE - Use Net_POP3's autodetection + * String 'DIGEST-MD5','CRAM-MD5','LOGIN','PLAIN','APOP','USER' + * - Attempt this authentication style first + * then fallback to autodetection. + * @var mixed + */ + var $method=true; + + // }}} + // {{{ Auth_Container_POP3() [constructor] + + /** + * Constructor of the container class + * + * @param $server string server or server:port combination + * @return object Returns an error object if something went wrong + */ + function Auth_Container_POP3($server=null) + { + if (isset($server) && !is_null($server)) { + if (is_array($server)) { + if (isset($server['host'])) { + $this->server = $server['host']; + } + if (isset($server['port'])) { + $this->port = $server['port']; + } + if (isset($server['method'])) { + $this->method = $server['method']; + } + } else { + if (strstr($server, ':')) { + $serverparts = explode(':', trim($server)); + $this->server = $serverparts[0]; + $this->port = $serverparts[1]; + } else { + $this->server = $server; + } + } + } + } + + // }}} + // {{{ fetchData() + + /** + * Try to login to the POP3 server + * + * @param string Username + * @param string Password + * @return boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_POP3::fetchData() called.', AUTH_LOG_DEBUG); + $pop3 =& new Net_POP3(); + $res = $pop3->connect($this->server, $this->port, $this->method); + if (!$res) { + $this->log('Connection to POP3 server failed.', AUTH_LOG_DEBUG); + return $res; + } + $result = $pop3->login($username, $password); + $pop3->disconnect(); + return $result; + } + + // }}} + +} +?> diff --git a/Auth/Container/RADIUS.php b/Auth/Container/RADIUS.php new file mode 100644 index 0000000..733a45c --- /dev/null +++ b/Auth/Container/RADIUS.php @@ -0,0 +1,182 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: RADIUS.php,v 1.16 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR Auth_RADIUS package + */ +require_once "Auth/RADIUS.php"; + +/** + * Storage driver for authenticating users against RADIUS servers. + * + * @category Authentication + * @package Auth + * @author Michael Bretterklieber + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.16 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.0 + */ +class Auth_Container_RADIUS extends Auth_Container +{ + + // {{{ properties + + /** + * Contains a RADIUS object + * @var object + */ + var $radius; + + /** + * Contains the authentication type + * @var string + */ + var $authtype; + + // }}} + // {{{ Auth_Container_RADIUS() [constructor] + + /** + * Constructor of the container class. + * + * $options can have these keys: + * 'servers' an array containing an array: servername, port, + * sharedsecret, timeout, maxtries + * 'configfile' The filename of the configuration file + * 'authtype' The type of authentication, one of: PAP, CHAP_MD5, + * MSCHAPv1, MSCHAPv2, default is PAP + * + * @param $options associative array + * @return object Returns an error object if something went wrong + */ + function Auth_Container_RADIUS($options) + { + $this->authtype = 'PAP'; + if (isset($options['authtype'])) { + $this->authtype = $options['authtype']; + } + $classname = 'Auth_RADIUS_' . $this->authtype; + if (!class_exists($classname)) { + PEAR::raiseError("Unknown Authtype, please use one of: " + ."PAP, CHAP_MD5, MSCHAPv1, MSCHAPv2!", 41, PEAR_ERROR_DIE); + } + + $this->radius = new $classname; + + if (isset($options['configfile'])) { + $this->radius->setConfigfile($options['configfile']); + } + + $servers = $options['servers']; + if (is_array($servers)) { + foreach ($servers as $server) { + $servername = $server[0]; + $port = isset($server[1]) ? $server[1] : 0; + $sharedsecret = isset($server[2]) ? $server[2] : 'testing123'; + $timeout = isset($server[3]) ? $server[3] : 3; + $maxtries = isset($server[4]) ? $server[4] : 3; + $this->radius->addServer($servername, $port, $sharedsecret, $timeout, $maxtries); + } + } + + if (!$this->radius->start()) { + PEAR::raiseError($this->radius->getError(), 41, PEAR_ERROR_DIE); + } + } + + // }}} + // {{{ fetchData() + + /** + * Authenticate + * + * @param string Username + * @param string Password + * @return bool true on success, false on reject + */ + function fetchData($username, $password, $challenge = null) + { + $this->log('Auth_Container_RADIUS::fetchData() called.', AUTH_LOG_DEBUG); + + switch($this->authtype) { + case 'CHAP_MD5': + case 'MSCHAPv1': + if (isset($challenge)) { + $this->radius->challenge = $challenge; + $this->radius->chapid = 1; + $this->radius->response = pack('H*', $password); + } else { + require_once 'Crypt/CHAP.php'; + $classname = 'Crypt_' . $this->authtype; + $crpt = new $classname; + $crpt->password = $password; + $this->radius->challenge = $crpt->challenge; + $this->radius->chapid = $crpt->chapid; + $this->radius->response = $crpt->challengeResponse(); + } + break; + + case 'MSCHAPv2': + require_once 'Crypt/CHAP.php'; + $crpt = new Crypt_MSCHAPv2; + $crpt->username = $username; + $crpt->password = $password; + $this->radius->challenge = $crpt->authChallenge; + $this->radius->peerChallenge = $crpt->peerChallenge; + $this->radius->chapid = $crpt->chapid; + $this->radius->response = $crpt->challengeResponse(); + break; + + default: + $this->radius->password = $password; + break; + } + + $this->radius->username = $username; + + $this->radius->putAuthAttributes(); + $result = $this->radius->send(); + if (PEAR::isError($result)) { + return false; + } + + $this->radius->getAttributes(); +// just for debugging +// $this->radius->dumpAttributes(); + + return $result; + } + + // }}} + +} +?> diff --git a/Auth/Container/SAP.php b/Auth/Container/SAP.php new file mode 100644 index 0000000..8b24e65 --- /dev/null +++ b/Auth/Container/SAP.php @@ -0,0 +1,179 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: SAP.php,v 1.5 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.4.0 + */ + +/** + * Include Auth_Container base class + */ +require_once 'Auth/Container.php'; +/** + * Include PEAR for error handling + */ +require_once 'PEAR.php'; + +/** + * Performs authentication against a SAP system using the SAPRFC PHP extension. + * + * When the option GETSSO2 is TRUE (default) + * the Single Sign-On (SSO) ticket is retrieved + * and stored as an Auth attribute called 'sap' + * in order to be reused for consecutive connections. + * + * @category Authentication + * @package Auth + * @author Stoyan Stefanov + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.5 $ + * @since Class available since Release 1.4.0 + */ +class Auth_Container_SAP extends Auth_Container { + + // {{{ properties + + /** + * @var array Default options + */ + var $options = array( + 'CLIENT' => '000', + 'LANG' => 'EN', + 'GETSSO2' => true, + ); + + // }}} + // {{{ Auth_Container_SAP() + + /** + * Class constructor. Checks that required options + * are present and that the SAPRFC extension is loaded + * + * Options that can be passed and their defaults: + *
+     * array(
+     *   'ASHOST' => "",
+     *   'SYSNR'  => "",
+     *   'CLIENT' => "000",
+     *   'GWHOST' =>"",
+     *   'GWSERV' =>"",
+     *   'MSHOST' =>"",
+     *   'R3NAME' =>"",
+     *   'GROUP'  =>"",
+     *   'LANG'   =>"EN",
+     *   'TRACE'  =>"",
+     *   'GETSSO2'=> true
+     * )
+     * 
+ * + * @param array array of options. + * @return void + */ + function Auth_Container_SAP($options) + { + $saprfc_loaded = PEAR::loadExtension('saprfc'); + if (!$saprfc_loaded) { + return PEAR::raiseError('Cannot use SAP authentication, ' + .'SAPRFC extension not loaded!'); + } + if (empty($options['R3NAME']) && empty($options['ASHOST'])) { + return PEAR::raiseError('R3NAME or ASHOST required for authentication'); + } + $this->options = array_merge($this->options, $options); + } + + // }}} + // {{{ fetchData() + + /** + * Performs username and password check + * + * @param string Username + * @param string Password + * @return boolean TRUE on success (valid user), FALSE otherwise + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_SAP::fetchData() called.', AUTH_LOG_DEBUG); + $connection_options = $this->options; + $connection_options['USER'] = $username; + $connection_options['PASSWD'] = $password; + $rfc = saprfc_open($connection_options); + if (!$rfc) { + $message = "Couldn't connect to the SAP system."; + $error = $this->getError(); + if ($error['message']) { + $message .= ': ' . $error['message']; + } + PEAR::raiseError($message, null, null, null, @$erorr['all']); + return false; + } else { + if (!empty($this->options['GETSSO2'])) { + $this->log('Attempting to retrieve SSO2 ticket.', AUTH_LOG_DEBUG); + if ($ticket = @saprfc_get_ticket($rfc)) { + $this->options['MYSAPSSO2'] = $ticket; + unset($this->options['GETSSO2']); + $this->_auth_obj->setAuthData('sap', $this->options); + } else { + PEAR::raiseError("SSO ticket retrieval failed"); + } + } + @saprfc_close($rfc); + return true; + } + + } + + // }}} + // {{{ getError() + + /** + * Retrieves the last error from the SAP connection + * and returns it as an array. + * + * @return array Array of error information + */ + function getError() + { + + $error = array(); + $sap_error = saprfc_error(); + if (empty($err)) { + return $error; + } + $err = explode("n", $sap_error); + foreach ($err AS $line) { + $item = split(':', $line); + $error[strtolower(trim($item[0]))] = trim($item[1]); + } + $error['all'] = $sap_error; + return $error; + } + + // }}} + +} + +?> diff --git a/Auth/Container/SMBPasswd.php b/Auth/Container/SMBPasswd.php new file mode 100644 index 0000000..b46c51e --- /dev/null +++ b/Auth/Container/SMBPasswd.php @@ -0,0 +1,182 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: SMBPasswd.php,v 1.8 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.3 + */ + +/** + * Include PEAR File_SMBPasswd + */ +require_once "File/SMBPasswd.php"; +/** + * Include Auth_Container Base file + */ +require_once "Auth/Container.php"; +/** + * Include PEAR class for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from an SAMBA smbpasswd file. + * + * This storage container can handle SAMBA smbpasswd files. + * + * Example: + * $a = new Auth("SMBPasswd", '/usr/local/private/smbpasswd'); + * $a->start(); + * if ($a->getAuth()) { + * printf ("AUTH OK
\n"); + * $a->logout(); + * } + * + * @category Authentication + * @package Auth + * @author Michael Bretterklieber + * @author Adam Ashley + * @package Auth + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.8 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.3 + */ +class Auth_Container_SMBPasswd extends Auth_Container +{ + + // {{{ properties + + /** + * File_SMBPasswd object + * @var object + */ + var $pwfile; + + // }}} + + // {{{ Auth_Container_SMBPasswd() [constructor] + + /** + * Constructor of the container class + * + * @param $filename string filename for a passwd type file + * @return object Returns an error object if something went wrong + */ + function Auth_Container_SMBPasswd($filename) + { + $this->pwfile = new File_SMBPasswd($filename,0); + + if (!$this->pwfile->load()) { + PEAR::raiseError("Error while reading file contents.", 41, PEAR_ERROR_DIE); + return; + } + + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from pwfile + * + * @param string Username + * @param string Password + * @return boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_SMBPasswd::fetchData() called.', AUTH_LOG_DEBUG); + return $this->pwfile->verifyAccount($username, $password); + } + + // }}} + // {{{ listUsers() + + function listUsers() + { + $this->log('Auth_Container_SMBPasswd::fetchData() called.', AUTH_LOG_DEBUG); + return $this->pwfile->getAccounts(); + } + + // }}} + // {{{ addUser() + + /** + * Add a new user to the storage container + * + * @param string Username + * @param string Password + * @param array Additional information + * + * @return boolean + */ + function addUser($username, $password, $additional = '') + { + $this->log('Auth_Container_SMBPasswd::addUser() called.', AUTH_LOG_DEBUG); + $res = $this->pwfile->addUser($user, $additional['userid'], $pass); + if ($res === true) { + return $this->pwfile->save(); + } + return $res; + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @param string Username + */ + function removeUser($username) + { + $this->log('Auth_Container_SMBPasswd::removeUser() called.', AUTH_LOG_DEBUG); + $res = $this->pwfile->delUser($username); + if ($res === true) { + return $this->pwfile->save(); + } + return $res; + } + + // }}} + // {{{ changePassword() + + /** + * Change password for user in the storage container + * + * @param string Username + * @param string The new password + */ + function changePassword($username, $password) + { + $this->log('Auth_Container_SMBPasswd::changePassword() called.', AUTH_LOG_DEBUG); + $res = $this->pwfile->modUser($username, '', $password); + if ($res === true) { + return $this->pwfile->save(); + } + return $res; + } + + // }}} + +} +?> diff --git a/Auth/Container/SOAP.php b/Auth/Container/SOAP.php new file mode 100644 index 0000000..f832d75 --- /dev/null +++ b/Auth/Container/SOAP.php @@ -0,0 +1,229 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: SOAP.php,v 1.13 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; +/** + * Include PEAR SOAP_Client + */ +require_once 'SOAP/Client.php'; + +/** + * Storage driver for fetching login data from SOAP + * + * This class takes one parameter (options), where + * you specify the following fields: endpoint, namespace, + * method, encoding, usernamefield and passwordfield. + * + * You can use specify features of your SOAP service + * by providing its parameters in an associative manner by + * using the '_features' array through the options parameter. + * + * The 'matchpassword' option should be set to false if your + * webservice doesn't return (username,password) pairs, but + * instead returns error when the login is invalid. + * + * Example usage: + * + * 'http://your.soap.service/endpoint', + * 'namespace' => 'urn:/Your/Namespace', + * 'method' => 'get', + * 'encoding' => 'UTF-8', + * 'usernamefield' => 'login', + * 'passwordfield' => 'password', + * 'matchpasswords' => false, + * '_features' => array ( + * 'example_feature' => 'example_value', + * 'another_example' => '' + * ) + * ); + * $auth = new Auth('SOAP', $options, 'loginFunction'); + * $auth->start(); + * + * ... + * + * ?> + * + * @category Authentication + * @package Auth + * @author Bruno Pedro + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.13 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.0 + */ +class Auth_Container_SOAP extends Auth_Container +{ + + // {{{ properties + + /** + * Required options for the class + * @var array + * @access private + */ + var $_requiredOptions = array( + 'endpoint', + 'namespace', + 'method', + 'encoding', + 'usernamefield', + 'passwordfield', + ); + + /** + * Options for the class + * @var array + * @access private + */ + var $_options = array(); + + /** + * Optional SOAP features + * @var array + * @access private + */ + var $_features = array(); + + /** + * The SOAP response + * @var array + * @access public + */ + var $soapResponse = array(); + + /** + * The SOAP client + * @var mixed + * @access public + */ + var $soapClient = null; + + // }}} + // {{{ Auth_Container_SOAP() [constructor] + + /** + * Constructor of the container class + * + * @param $options, associative array with endpoint, namespace, method, + * usernamefield, passwordfield and optional features + */ + function Auth_Container_SOAP($options) + { + $this->_options = $options; + if (!isset($this->_options['matchpasswords'])) { + $this->_options['matchpasswords'] = true; + } + if (!empty($this->_options['_features'])) { + $this->_features = $this->_options['_features']; + unset($this->_options['_features']); + } + } + + // }}} + // {{{ fetchData() + + /** + * Fetch data from SOAP service + * + * Requests the SOAP service for the given username/password + * combination. + * + * @param string Username + * @param string Password + * @return mixed Returns the SOAP response or false if something went wrong + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_SOAP::fetchData() called.', AUTH_LOG_DEBUG); + // check if all required options are set + if (array_intersect($this->_requiredOptions, array_keys($this->_options)) != $this->_requiredOptions) { + return false; + } else { + // create a SOAP client and set encoding + $this->soapClient = new SOAP_Client($this->_options['endpoint']); + $this->soapClient->setEncoding($this->_options['encoding']); + } + + // set the trace option if requested + if (isset($this->_options['trace'])) { + $this->soapClient->__options['trace'] = true; + } + + // set the timeout option if requested + if (isset($this->_options['timeout'])) { + $this->soapClient->__options['timeout'] = $this->_options['timeout']; + } + + // assign username and password fields + $usernameField = new SOAP_Value($this->_options['usernamefield'],'string', $username); + $passwordField = new SOAP_Value($this->_options['passwordfield'],'string', $password); + $SOAPParams = array($usernameField, $passwordField); + + // assign optional features + foreach ($this->_features as $fieldName => $fieldValue) { + $SOAPParams[] = new SOAP_Value($fieldName, 'string', $fieldValue); + } + + // make SOAP call + $this->soapResponse = $this->soapClient->call( + $this->_options['method'], + $SOAPParams, + array('namespace' => $this->_options['namespace']) + ); + + if (!PEAR::isError($this->soapResponse)) { + if ($this->_options['matchpasswords']) { + // check if passwords match + if ($password == $this->soapResponse->{$this->_options['passwordfield']}) { + return true; + } else { + return false; + } + } else { + return true; + } + } else { + return false; + } + } + + // }}} + +} +?> diff --git a/Auth/Container/SOAP5.php b/Auth/Container/SOAP5.php new file mode 100644 index 0000000..c98ee58 --- /dev/null +++ b/Auth/Container/SOAP5.php @@ -0,0 +1,268 @@ + + * @author Marcel Oelke + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: SOAP5.php,v 1.9 2007/07/02 08:25:41 aashley Exp $ + * @since File available since Release 1.4.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from SOAP using the PHP5 Builtin SOAP + * functions. This is a modification of the SOAP Storage driver from Bruno Pedro + * thats using the PEAR SOAP Package. + * + * This class takes one parameter (options), where + * you specify the following fields: + * * location and uri, or wsdl file + * * method to call on the SOAP service + * * usernamefield, the name of the parameter where the username is supplied + * * passwordfield, the name of the parameter where the password is supplied + * * matchpassword, whether to look for the password in the response from + * the function call or assume that no errors means user + * authenticated. + * + * See http://www.php.net/manual/en/ref.soap.php for further details + * on options for the PHP5 SoapClient which are passed through. + * + * Example usage without WSDL: + * + * NULL, + * 'location' => 'http://your.soap.service/endpoint', + * 'uri' => 'urn:/Your/Namespace', + * 'method' => 'checkAuth', + * 'usernamefield' => 'username', + * 'passwordfield' => 'password', + * 'matchpasswords' => false, + * '_features' => array ( + * 'extra_parameter' => 'example_value', + * 'another_parameter' => 'foobar' + * ) + * ); + * + * $auth = new Auth('SOAP5', $options); + * $auth->start(); + * + * ?> + * + * Example usage with WSDL: + * + * 'http://your.soap.service/wsdl', + * 'method' => 'checkAuth', + * 'usernamefield' => 'username', + * 'passwordfield' => 'password', + * 'matchpasswords' => false, + * '_features' => array ( + * 'extra_parameter' => 'example_value', + * 'another_parameter' => 'foobar' + * ) + * ); + * + * $auth = new Auth('SOAP5', $options); + * $auth->start(); + * + * ?> + * + * @category Authentication + * @package Auth + * @author Based upon Auth_Container_SOAP by Bruno Pedro + * @author Marcel Oelke + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.9 $ + * @since Class available since Release 1.4.0 + */ +class Auth_Container_SOAP5 extends Auth_Container +{ + + // {{{ properties + + /** + * Required options for the class + * @var array + * @access private + */ + var $_requiredOptions = array( + 'location', + 'uri', + 'method', + 'usernamefield', + 'passwordfield', + 'wsdl', + ); + + /** + * Options for the class + * @var array + * @access private + */ + var $_options = array(); + + /** + * Optional SOAP features + * @var array + * @access private + */ + var $_features = array(); + + /** + * The SOAP response + * @var array + * @access public + */ + var $soapResponse = array(); + + // }}} + // {{{ Auth_Container_SOAP5() + + /** + * Constructor of the container class + * + * @param $options, associative array with endpoint, namespace, method, + * usernamefield, passwordfield and optional features + */ + function Auth_Container_SOAP5($options) + { + $this->_setDefaults(); + + foreach ($options as $name => $value) { + $this->_options[$name] = $value; + } + + if (!empty($this->_options['_features'])) { + $this->_features = $this->_options['_features']; + unset($this->_options['_features']); + } + } + + // }}} + // {{{ fetchData() + + /** + * Fetch data from SOAP service + * + * Requests the SOAP service for the given username/password + * combination. + * + * @param string Username + * @param string Password + * @return mixed Returns the SOAP response or false if something went wrong + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_SOAP5::fetchData() called.', AUTH_LOG_DEBUG); + $result = $this->_validateOptions(); + if (PEAR::isError($result)) + return $result; + + // create a SOAP client + $soapClient = new SoapClient($this->_options["wsdl"], $this->_options); + + $params = array(); + // first, assign the optional features + foreach ($this->_features as $fieldName => $fieldValue) { + $params[$fieldName] = $fieldValue; + } + // assign username and password ... + $params[$this->_options['usernamefield']] = $username; + $params[$this->_options['passwordfield']] = $password; + + try { + $this->soapResponse = $soapClient->__soapCall($this->_options['method'], $params); + + if ($this->_options['matchpasswords']) { + // check if passwords match + if ($password == $this->soapResponse[$this->_options['passwordfield']]) { + return true; + } else { + return false; + } + } else { + return true; + } + } catch (SoapFault $e) { + return PEAR::raiseError("Error retrieving authentication data. Received SOAP Fault: ".$e->faultstring, $e->faultcode); + } + } + + // }}} + // {{{ _validateOptions() + + /** + * Validate that the options passed to the container class are enough for us to proceed + * + * @access private + * @param array + */ + function _validateOptions() + { + if ( ( is_null($this->_options['wsdl']) + && is_null($this->_options['location']) + && is_null($this->_options['uri'])) + || ( is_null($this->_options['wsdl']) + && ( is_null($this->_options['location']) + || is_null($this->_options['uri'])))) { + return PEAR::raiseError('Either a WSDL file or a location/uri pair must be specified.'); + } + if (is_null($this->_options['method'])) { + return PEAR::raiseError('A method to call on the soap service must be specified.'); + } + return true; + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->_options['wsdl'] = null; + $this->_options['location'] = null; + $this->_options['uri'] = null; + $this->_options['method'] = null; + $this->_options['usernamefield'] = 'username'; + $this->_options['passwordfield'] = 'password'; + $this->_options['matchpasswords'] = true; + } + + // }}} + +} +?> diff --git a/Auth/Container/vpopmail.php b/Auth/Container/vpopmail.php new file mode 100644 index 0000000..f73c55e --- /dev/null +++ b/Auth/Container/vpopmail.php @@ -0,0 +1,88 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: vpopmail.php,v 1.10 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.2.0 + */ + +/** + * Include Auth_Container base class + */ +require_once "Auth/Container.php"; +/** + * Include PEAR package for error handling + */ +require_once "PEAR.php"; + +/** + * Storage driver for fetching login data from vpopmail + * + * @category Authentication + * @package Auth + * @author Stanislav Grozev + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.10 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.2.0 + */ +class Auth_Container_vpopmail extends Auth_Container { + + // {{{ Constructor + + /** + * Constructor of the container class + * + * @return void + */ + function Auth_Container_vpopmail() + { + if (!extension_loaded('vpopmail')) { + return PEAR::raiseError('Cannot use VPOPMail authentication, ' + .'VPOPMail extension not loaded!', 41, PEAR_ERROR_DIE); + } + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from vpopmail + * + * @param string Username - has to be valid email address + * @param string Password + * @return boolean + */ + function fetchData($username, $password) + { + $this->log('Auth_Container_vpopmail::fetchData() called.', AUTH_LOG_DEBUG); + $userdata = array(); + $userdata = preg_split("/@/", $username, 2); + $result = @vpopmail_auth_user($userdata[0], $userdata[1], $password); + + return $result; + } + + // }}} + +} +?> diff --git a/Auth/Controller.php b/Auth/Controller.php new file mode 100644 index 0000000..2dcb583 --- /dev/null +++ b/Auth/Controller.php @@ -0,0 +1,302 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Controller.php,v 1.11 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Controlls access to a group of php access + * and redirects to a predefined login page as + * needed + * + * In all pages + * + * include_once('Auth.php'); + * include_once('Auth/Controller.php'); + * $_auth = new Auth('File', 'passwd'); + * $authController = new Auth_Controller($_auth, 'login.php', 'index.php'); + * $authController->start(); + * + * + * In login.php + * + * include_once('Auth.php'); + * include_once('Auth/Controller.php'); + * $_auth = new Auth('File', 'passwd'); + * $authController = new Auth_Controller($_auth, 'login.php', 'index.php'); + * $authController->start(); + * if( $authController->isAuthorised() ){ + * $authController->redirectBack(); + * } + * + * + * @category Authentication + * @author Yavor Shahpasov + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.11 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Controller +{ + + // {{{ properties + + /** + * The Auth instance this controller is managing + * + * @var object Auth + */ + var $auth = null; + + /** + * The login URL + * @var string + * */ + var $login = null; + + /** + * The default index page to use when the caller page is not set + * + * @var string + */ + var $default = null; + + /** + * If this is set to true after a succesfull login the + * Auth_Controller::redirectBack() is invoked automatically + * + * @var boolean + */ + var $autoRedirectBack = false; + + // }}} + // {{{ Auth_Controller() [constructor] + + /** + * Constructor + * + * @param Auth An auth instance + * @param string The login page + * @param string The default page to go to if return page is not set + * @param array Some rules about which urls need to be sent to the login page + * @return void + * @todo Add a list of urls which need redirection + */ + function Auth_Controller(&$auth_obj, $login='login.php', $default='index.php', $accessList=array()) + { + $this->auth =& $auth_obj; + $this->_loginPage = $login; + $this->_defaultPage = $default; + @session_start(); + if (!empty($_GET['return']) && $_GET['return'] && !strstr($_GET['return'], $this->_loginPage)) { + $this->auth->setAuthData('returnUrl', $_GET['return']); + } + + if(!empty($_GET['authstatus']) && $this->auth->status == '') { + $this->auth->status = $_GET['authstatus']; + } + } + + // }}} + // {{{ setAutoRedirectBack() + + /** + * Enables auto redirection when login is done + * + * @param bool Sets the autoRedirectBack flag to this + * @see Auth_Controller::autoRedirectBack + * @return void + */ + function setAutoRedirectBack($flag = true) + { + $this->autoRedirectBack = $flag; + } + + // }}} + // {{{ redirectBack() + + /** + * Redirects Back to the calling page + * + * @return void + */ + function redirectBack() + { + // If redirectback go there + // else go to the default page + + $returnUrl = $this->auth->getAuthData('returnUrl'); + if(!$returnUrl) { + $returnUrl = $this->_defaultPage; + } + + // Add some entropy to the return to make it unique + // avoind problems with cached pages and proxies + if(strpos($returnUrl, '?') === false) { + $returnUrl .= '?'; + } + $returnUrl .= uniqid(''); + + // Track the auth status + if($this->auth->status != '') { + $url .= '&authstatus='.$this->auth->status; + } + header('Location:'.$returnUrl); + print("You could not be redirected to $returnUrl"); + } + + // }}} + // {{{ redirectLogin() + + /** + * Redirects to the login Page if not authorised + * + * put return page on the query or in auth + * + * @return void + */ + function redirectLogin() + { + // Go to the login Page + + // For Auth, put some check to avoid infinite redirects, this should at least exclude + // the login page + + $url = $this->_loginPage; + if(strpos($url, '?') === false) { + $url .= '?'; + } + + if(!strstr($_SERVER['PHP_SELF'], $this->_loginPage)) { + $url .= 'return='.urlencode($_SERVER['PHP_SELF']); + } + + // Track the auth status + if($this->auth->status != '') { + $url .= '&authstatus='.$this->auth->status; + } + + header('Location:'.$url); + print("You could not be redirected to $url"); + } + + // }}} + // {{{ start() + + /** + * Starts the Auth Procedure + * + * If the page requires login the user is redirected to the login page + * otherwise the Auth::start is called to initialize Auth + * + * @return void + * @todo Implement an access list which specifies which urls/pages need login and which do not + */ + function start() + { + // Check the accessList here + // ACL should be a list of urls with allow/deny + // If allow set allowLogin to false + // Some wild card matching should be implemented ?,* + if(!strstr($_SERVER['PHP_SELF'], $this->_loginPage) && !$this->auth->checkAuth()) { + $this->redirectLogin(); + } else { + $this->auth->start(); + // Logged on and on login page + if(strstr($_SERVER['PHP_SELF'], $this->_loginPage) && $this->auth->checkAuth()){ + $this->autoRedirectBack ? + $this->redirectBack() : + null ; + } + } + + + } + + // }}} + // {{{ isAuthorised() + + /** + * Checks is the user is logged on + * @see Auth::checkAuth() + */ + function isAuthorised() + { + return($this->auth->checkAuth()); + } + + // }}} + // {{{ checkAuth() + + /** + * Proxy call to auth + * @see Auth::checkAuth() + */ + function checkAuth() + { + return($this->auth->checkAuth()); + } + + // }}} + // {{{ logout() + + /** + * Proxy call to auth + * @see Auth::logout() + */ + function logout() + { + return($this->auth->logout()); + } + + // }}} + // {{{ getUsername() + + /** + * Proxy call to auth + * @see Auth::getUsername() + */ + function getUsername() + { + return($this->auth->getUsername()); + } + + // }}} + // {{{ getStatus() + + /** + * Proxy call to auth + * @see Auth::getStatus() + */ + function getStatus() + { + return($this->auth->getStatus()); + } + + // }}} + +} + +?> diff --git a/Auth/Frontend/Html.php b/Auth/Frontend/Html.php new file mode 100644 index 0000000..ece81f6 --- /dev/null +++ b/Auth/Frontend/Html.php @@ -0,0 +1,142 @@ + + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Html.php,v 1.11 2007/06/12 03:11:26 aashley Exp $ + * @link http://pear.php.net/package/Auth + * @since File available since Release 1.3.0 + */ + +/** + * Standard Html Login form + * + * @category Authentication + * @package Auth + * @author Yavor Shahpasov + * @author Adam Ashley + * @copyright 2001-2006 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version Release: 1.5.4 File: $Revision: 1.11 $ + * @link http://pear.php.net/package/Auth + * @since Class available since Release 1.3.0 + */ +class Auth_Frontend_Html { + + // {{{ render() + + /** + * Displays the login form + * + * @param object The calling auth instance + * @param string The previously used username + * @return void + */ + function render(&$caller, $username = '') { + $loginOnClick = 'return true;'; + + // Try To Use Challene response + // TODO javascript might need some improvement for work on other browsers + if($caller->advancedsecurity && $caller->storage->supportsChallengeResponse() ) { + + // Init the secret cookie + $caller->session['loginchallenege'] = md5(microtime()); + + print "\n"; + print ''."\n";; + print "\n"; + + $loginOnClick = ' return securePassword(); '; + } + + print '
'."\n"; + + $status = ''; + if (!empty($caller->status) && $caller->status == AUTH_EXPIRED) { + $status = 'Your session has expired. Please login again!'."\n"; + } else if (!empty($caller->status) && $caller->status == AUTH_IDLED) { + $status = 'You have been idle for too long. Please login again!'."\n"; + } else if (!empty ($caller->status) && $caller->status == AUTH_WRONG_LOGIN) { + $status = 'Wrong login data!'."\n"; + } else if (!empty ($caller->status) && $caller->status == AUTH_SECURITY_BREACH) { + $status = 'Security problem detected. '."\n"; + } + + print '
'."\n"; + print ''."\n"; + print ''."\n"; + print ' '."\n"; + print ''."\n"; + print ''."\n"; + print ' '."\n"; + print ' '."\n"; + print ''."\n"; + print ''."\n"; + print ' '."\n"; + print ' '."\n"; + print ''."\n"; + print ''."\n"; + + //onClick=" '.$loginOnClick.' " + print ' '."\n"; + print ''."\n"; + print '
Login ' + .$status.'
Username:
Password:
'."\n"; + + // Might be a good idea to make the variable name variable + print ''; + print '
'."\n"; + print '
'."\n"; + } + + // }}} + +} + +?> diff --git a/Auth/Frontend/md5.js b/Auth/Frontend/md5.js new file mode 100644 index 0000000..46d2aab --- /dev/null +++ b/Auth/Frontend/md5.js @@ -0,0 +1,256 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} +function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} +function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} +function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } +function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } +function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function md5_vm_test() +{ + return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ +function core_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); + +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Calculate the HMAC-MD5, of a key and some data + */ +function core_hmac_md5(key, data) +{ + var bkey = str2binl(key); + if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ +function str2binl(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + return bin; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + return str; +} + +/* + * Convert an array of little-endian words to a hex string. + */ +function binl2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of little-endian words to a base-64 string + */ +function binl2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} diff --git a/Cache/Lite.php b/Cache/Lite.php new file mode 100644 index 0000000..7bab03b --- /dev/null +++ b/Cache/Lite.php @@ -0,0 +1,846 @@ + +* +* Nota : A chinese documentation (thanks to RainX ) is +* available at : +* http://rainx.phpmore.com/manual/cache_lite.html +* +* @package Cache_Lite +* @category Caching +* @version $Id: Lite.php 308851 2011-03-02 11:01:42Z tacker $ +* @author Fabien MARTY +*/ + +define('CACHE_LITE_ERROR_RETURN', 1); +define('CACHE_LITE_ERROR_DIE', 8); + +class Cache_Lite +{ + + // --- Private properties --- + + /** + * Directory where to put the cache files + * (make sure to add a trailing slash) + * + * @var string $_cacheDir + */ + var $_cacheDir = '/tmp/'; + + /** + * Enable / disable caching + * + * (can be very usefull for the debug of cached scripts) + * + * @var boolean $_caching + */ + var $_caching = true; + + /** + * Cache lifetime (in seconds) + * + * If null, the cache is valid forever. + * + * @var int $_lifeTime + */ + var $_lifeTime = 3600; + + /** + * Enable / disable fileLocking + * + * (can avoid cache corruption under bad circumstances) + * + * @var boolean $_fileLocking + */ + var $_fileLocking = true; + + /** + * Timestamp of the last valid cache + * + * @var int $_refreshTime + */ + var $_refreshTime; + + /** + * File name (with path) + * + * @var string $_file + */ + var $_file; + + /** + * File name (without path) + * + * @var string $_fileName + */ + var $_fileName; + + /** + * Enable / disable write control (the cache is read just after writing to detect corrupt entries) + * + * Enable write control will lightly slow the cache writing but not the cache reading + * Write control can detect some corrupt cache files but maybe it's not a perfect control + * + * @var boolean $_writeControl + */ + var $_writeControl = true; + + /** + * Enable / disable read control + * + * If enabled, a control key is embeded in cache file and this key is compared with the one + * calculated after the reading. + * + * @var boolean $_writeControl + */ + var $_readControl = true; + + /** + * Type of read control (only if read control is enabled) + * + * Available values are : + * 'md5' for a md5 hash control (best but slowest) + * 'crc32' for a crc32 hash control (lightly less safe but faster, better choice) + * 'strlen' for a length only test (fastest) + * + * @var boolean $_readControlType + */ + var $_readControlType = 'crc32'; + + /** + * Pear error mode (when raiseError is called) + * + * (see PEAR doc) + * + * @see setToDebug() + * @var int $_pearErrorMode + */ + var $_pearErrorMode = CACHE_LITE_ERROR_RETURN; + + /** + * Current cache id + * + * @var string $_id + */ + var $_id; + + /** + * Current cache group + * + * @var string $_group + */ + var $_group; + + /** + * Enable / Disable "Memory Caching" + * + * NB : There is no lifetime for memory caching ! + * + * @var boolean $_memoryCaching + */ + var $_memoryCaching = false; + + /** + * Enable / Disable "Only Memory Caching" + * (be carefull, memory caching is "beta quality") + * + * @var boolean $_onlyMemoryCaching + */ + var $_onlyMemoryCaching = false; + + /** + * Memory caching array + * + * @var array $_memoryCachingArray + */ + var $_memoryCachingArray = array(); + + /** + * Memory caching counter + * + * @var int $memoryCachingCounter + */ + var $_memoryCachingCounter = 0; + + /** + * Memory caching limit + * + * @var int $memoryCachingLimit + */ + var $_memoryCachingLimit = 1000; + + /** + * File Name protection + * + * if set to true, you can use any cache id or group name + * if set to false, it can be faster but cache ids and group names + * will be used directly in cache file names so be carefull with + * special characters... + * + * @var boolean $fileNameProtection + */ + var $_fileNameProtection = true; + + /** + * Enable / disable automatic serialization + * + * it can be used to save directly datas which aren't strings + * (but it's slower) + * + * @var boolean $_serialize + */ + var $_automaticSerialization = false; + + /** + * Disable / Tune the automatic cleaning process + * + * The automatic cleaning process destroy too old (for the given life time) + * cache files when a new cache file is written. + * 0 => no automatic cache cleaning + * 1 => systematic cache cleaning + * x (integer) > 1 => automatic cleaning randomly 1 times on x cache write + * + * @var int $_automaticCleaning + */ + var $_automaticCleaningFactor = 0; + + /** + * Nested directory level + * + * Set the hashed directory structure level. 0 means "no hashed directory + * structure", 1 means "one level of directory", 2 means "two levels"... + * This option can speed up Cache_Lite only when you have many thousands of + * cache file. Only specific benchs can help you to choose the perfect value + * for you. Maybe, 1 or 2 is a good start. + * + * @var int $_hashedDirectoryLevel + */ + var $_hashedDirectoryLevel = 0; + + /** + * Umask for hashed directory structure + * + * @var int $_hashedDirectoryUmask + */ + var $_hashedDirectoryUmask = 0700; + + /** + * API break for error handling in CACHE_LITE_ERROR_RETURN mode + * + * In CACHE_LITE_ERROR_RETURN mode, error handling was not good because + * for example save() method always returned a boolean (a PEAR_Error object + * would be better in CACHE_LITE_ERROR_RETURN mode). To correct this without + * breaking the API, this option (false by default) can change this handling. + * + * @var boolean + */ + var $_errorHandlingAPIBreak = false; + + // --- Public methods --- + + /** + * Constructor + * + * $options is an assoc. Available options are : + * $options = array( + * 'cacheDir' => directory where to put the cache files (string), + * 'caching' => enable / disable caching (boolean), + * 'lifeTime' => cache lifetime in seconds (int), + * 'fileLocking' => enable / disable fileLocking (boolean), + * 'writeControl' => enable / disable write control (boolean), + * 'readControl' => enable / disable read control (boolean), + * 'readControlType' => type of read control 'crc32', 'md5', 'strlen' (string), + * 'pearErrorMode' => pear error mode (when raiseError is called) (cf PEAR doc) (int), + * 'memoryCaching' => enable / disable memory caching (boolean), + * 'onlyMemoryCaching' => enable / disable only memory caching (boolean), + * 'memoryCachingLimit' => max nbr of records to store into memory caching (int), + * 'fileNameProtection' => enable / disable automatic file name protection (boolean), + * 'automaticSerialization' => enable / disable automatic serialization (boolean), + * 'automaticCleaningFactor' => distable / tune automatic cleaning process (int), + * 'hashedDirectoryLevel' => level of the hashed directory system (int), + * 'hashedDirectoryUmask' => umask for hashed directory structure (int), + * 'errorHandlingAPIBreak' => API break for better error handling ? (boolean) + * ); + * + * If sys_get_temp_dir() is available and the + * 'cacheDir' option is not provided in the + * constructor options array its output is used + * to determine the suitable temporary directory. + * + * @see http://de.php.net/sys_get_temp_dir + * @see http://pear.php.net/bugs/bug.php?id=18328 + * + * @param array $options options + * @access public + */ + function Cache_Lite($options = array(NULL)) + { + foreach($options as $key => $value) { + $this->setOption($key, $value); + } + if (!isset($options['cacheDir']) && function_exists('sys_get_temp_dir')) { + $this->setOption('cacheDir', sys_get_temp_dir() . DIRECTORY_SEPARATOR); + } + } + + /** + * Generic way to set a Cache_Lite option + * + * see Cache_Lite constructor for available options + * + * @var string $name name of the option + * @var mixed $value value of the option + * @access public + */ + function setOption($name, $value) + { + $availableOptions = array('errorHandlingAPIBreak', 'hashedDirectoryUmask', 'hashedDirectoryLevel', 'automaticCleaningFactor', 'automaticSerialization', 'fileNameProtection', 'memoryCaching', 'onlyMemoryCaching', 'memoryCachingLimit', 'cacheDir', 'caching', 'lifeTime', 'fileLocking', 'writeControl', 'readControl', 'readControlType', 'pearErrorMode'); + if (in_array($name, $availableOptions)) { + $property = '_'.$name; + $this->$property = $value; + } + } + + /** + * Test if a cache is available and (if yes) return it + * + * @param string $id cache id + * @param string $group name of the cache group + * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested + * @return string data of the cache (else : false) + * @access public + */ + function get($id, $group = 'default', $doNotTestCacheValidity = false) + { + $this->_id = $id; + $this->_group = $group; + $data = false; + if ($this->_caching) { + $this->_setRefreshTime(); + $this->_setFileName($id, $group); + clearstatcache(); + if ($this->_memoryCaching) { + if (isset($this->_memoryCachingArray[$this->_file])) { + if ($this->_automaticSerialization) { + return unserialize($this->_memoryCachingArray[$this->_file]); + } + return $this->_memoryCachingArray[$this->_file]; + } + if ($this->_onlyMemoryCaching) { + return false; + } + } + if (($doNotTestCacheValidity) || (is_null($this->_refreshTime))) { + if (file_exists($this->_file)) { + $data = $this->_read(); + } + } else { + if ((file_exists($this->_file)) && (@filemtime($this->_file) > $this->_refreshTime)) { + $data = $this->_read(); + } + } + if (($data) and ($this->_memoryCaching)) { + $this->_memoryCacheAdd($data); + } + if (($this->_automaticSerialization) and (is_string($data))) { + $data = unserialize($data); + } + return $data; + } + return false; + } + + /** + * Save some data in a cache file + * + * @param string $data data to put in cache (can be another type than strings if automaticSerialization is on) + * @param string $id cache id + * @param string $group name of the cache group + * @return boolean true if no problem (else : false or a PEAR_Error object) + * @access public + */ + function save($data, $id = NULL, $group = 'default') + { + if ($this->_caching) { + if ($this->_automaticSerialization) { + $data = serialize($data); + } + if (isset($id)) { + $this->_setFileName($id, $group); + } + if ($this->_memoryCaching) { + $this->_memoryCacheAdd($data); + if ($this->_onlyMemoryCaching) { + return true; + } + } + if ($this->_automaticCleaningFactor>0 && ($this->_automaticCleaningFactor==1 || mt_rand(1, $this->_automaticCleaningFactor)==1)) { + $this->clean(false, 'old'); + } + if ($this->_writeControl) { + $res = $this->_writeAndControl($data); + if (is_bool($res)) { + if ($res) { + return true; + } + // if $res if false, we need to invalidate the cache + @touch($this->_file, time() - 2*abs($this->_lifeTime)); + return false; + } + } else { + $res = $this->_write($data); + } + if (is_object($res)) { + // $res is a PEAR_Error object + if (!($this->_errorHandlingAPIBreak)) { + return false; // we return false (old API) + } + } + return $res; + } + return false; + } + + /** + * Remove a cache file + * + * @param string $id cache id + * @param string $group name of the cache group + * @param boolean $checkbeforeunlink check if file exists before removing it + * @return boolean true if no problem + * @access public + */ + function remove($id, $group = 'default', $checkbeforeunlink = false) + { + $this->_setFileName($id, $group); + if ($this->_memoryCaching) { + if (isset($this->_memoryCachingArray[$this->_file])) { + unset($this->_memoryCachingArray[$this->_file]); + $this->_memoryCachingCounter = $this->_memoryCachingCounter - 1; + } + if ($this->_onlyMemoryCaching) { + return true; + } + } + if ( $checkbeforeunlink ) { + if (!file_exists($this->_file)) return true; + } + return $this->_unlink($this->_file); + } + + /** + * Clean the cache + * + * if no group is specified all cache files will be destroyed + * else only cache files of the specified group will be destroyed + * + * @param string $group name of the cache group + * @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup', + * 'callback_myFunction' + * @return boolean true if no problem + * @access public + */ + function clean($group = false, $mode = 'ingroup') + { + return $this->_cleanDir($this->_cacheDir, $group, $mode); + } + + /** + * Set to debug mode + * + * When an error is found, the script will stop and the message will be displayed + * (in debug mode only). + * + * @access public + */ + function setToDebug() + { + $this->setOption('pearErrorMode', CACHE_LITE_ERROR_DIE); + } + + /** + * Set a new life time + * + * @param int $newLifeTime new life time (in seconds) + * @access public + */ + function setLifeTime($newLifeTime) + { + $this->_lifeTime = $newLifeTime; + $this->_setRefreshTime(); + } + + /** + * Save the state of the caching memory array into a cache file cache + * + * @param string $id cache id + * @param string $group name of the cache group + * @access public + */ + function saveMemoryCachingState($id, $group = 'default') + { + if ($this->_caching) { + $array = array( + 'counter' => $this->_memoryCachingCounter, + 'array' => $this->_memoryCachingArray + ); + $data = serialize($array); + $this->save($data, $id, $group); + } + } + + /** + * Load the state of the caching memory array from a given cache file cache + * + * @param string $id cache id + * @param string $group name of the cache group + * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested + * @access public + */ + function getMemoryCachingState($id, $group = 'default', $doNotTestCacheValidity = false) + { + if ($this->_caching) { + if ($data = $this->get($id, $group, $doNotTestCacheValidity)) { + $array = unserialize($data); + $this->_memoryCachingCounter = $array['counter']; + $this->_memoryCachingArray = $array['array']; + } + } + } + + /** + * Return the cache last modification time + * + * BE CAREFUL : THIS METHOD IS FOR HACKING ONLY ! + * + * @return int last modification time + */ + function lastModified() + { + return @filemtime($this->_file); + } + + /** + * Trigger a PEAR error + * + * To improve performances, the PEAR.php file is included dynamically. + * The file is so included only when an error is triggered. So, in most + * cases, the file isn't included and perfs are much better. + * + * @param string $msg error message + * @param int $code error code + * @access public + */ + function raiseError($msg, $code) + { + include_once('PEAR.php'); + return PEAR::raiseError($msg, $code, $this->_pearErrorMode); + } + + /** + * Extend the life of a valid cache file + * + * see http://pear.php.net/bugs/bug.php?id=6681 + * + * @access public + */ + function extendLife() + { + @touch($this->_file); + } + + // --- Private methods --- + + /** + * Compute & set the refresh time + * + * @access private + */ + function _setRefreshTime() + { + if (is_null($this->_lifeTime)) { + $this->_refreshTime = null; + } else { + $this->_refreshTime = time() - $this->_lifeTime; + } + } + + /** + * Remove a file + * + * @param string $file complete file path and name + * @return boolean true if no problem + * @access private + */ + function _unlink($file) + { + if (!@unlink($file)) { + return $this->raiseError('Cache_Lite : Unable to remove cache !', -3); + } + return true; + } + + /** + * Recursive function for cleaning cache file in the given directory + * + * @param string $dir directory complete path (with a trailing slash) + * @param string $group name of the cache group + * @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup', + 'callback_myFunction' + * @return boolean true if no problem + * @access private + */ + function _cleanDir($dir, $group = false, $mode = 'ingroup') + { + if ($this->_fileNameProtection) { + $motif = ($group) ? 'cache_'.md5($group).'_' : 'cache_'; + } else { + $motif = ($group) ? 'cache_'.$group.'_' : 'cache_'; + } + if ($this->_memoryCaching) { + foreach($this->_memoryCachingArray as $key => $v) { + if (strpos($key, $motif) !== false) { + unset($this->_memoryCachingArray[$key]); + $this->_memoryCachingCounter = $this->_memoryCachingCounter - 1; + } + } + if ($this->_onlyMemoryCaching) { + return true; + } + } + if (!($dh = opendir($dir))) { + return $this->raiseError('Cache_Lite : Unable to open cache directory !', -4); + } + $result = true; + while ($file = readdir($dh)) { + if (($file != '.') && ($file != '..')) { + if (substr($file, 0, 6)=='cache_') { + $file2 = $dir . $file; + if (is_file($file2)) { + switch (substr($mode, 0, 9)) { + case 'old': + // files older than lifeTime get deleted from cache + if (!is_null($this->_lifeTime)) { + if ((time() - @filemtime($file2)) > $this->_lifeTime) { + $result = ($result and ($this->_unlink($file2))); + } + } + break; + case 'notingrou': + if (strpos($file2, $motif) === false) { + $result = ($result and ($this->_unlink($file2))); + } + break; + case 'callback_': + $func = substr($mode, 9, strlen($mode) - 9); + if ($func($file2, $group)) { + $result = ($result and ($this->_unlink($file2))); + } + break; + case 'ingroup': + default: + if (strpos($file2, $motif) !== false) { + $result = ($result and ($this->_unlink($file2))); + } + break; + } + } + if ((is_dir($file2)) and ($this->_hashedDirectoryLevel>0)) { + $result = ($result and ($this->_cleanDir($file2 . '/', $group, $mode))); + } + } + } + } + return $result; + } + + /** + * Add some date in the memory caching array + * + * @param string $data data to cache + * @access private + */ + function _memoryCacheAdd($data) + { + $this->_memoryCachingArray[$this->_file] = $data; + if ($this->_memoryCachingCounter >= $this->_memoryCachingLimit) { + list($key, ) = each($this->_memoryCachingArray); + unset($this->_memoryCachingArray[$key]); + } else { + $this->_memoryCachingCounter = $this->_memoryCachingCounter + 1; + } + } + + /** + * Make a file name (with path) + * + * @param string $id cache id + * @param string $group name of the group + * @access private + */ + function _setFileName($id, $group) + { + + if ($this->_fileNameProtection) { + $suffix = 'cache_'.md5($group).'_'.md5($id); + } else { + $suffix = 'cache_'.$group.'_'.$id; + } + $root = $this->_cacheDir; + if ($this->_hashedDirectoryLevel>0) { + $hash = md5($suffix); + for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) { + $root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/'; + } + } + $this->_fileName = $suffix; + $this->_file = $root.$suffix; + } + + /** + * Read the cache file and return the content + * + * @return string content of the cache file (else : false or a PEAR_Error object) + * @access private + */ + function _read() + { + $fp = @fopen($this->_file, "rb"); + if ($this->_fileLocking) @flock($fp, LOCK_SH); + if ($fp) { + clearstatcache(); + $length = @filesize($this->_file); + $mqr = get_magic_quotes_runtime(); + if ($mqr) { + set_magic_quotes_runtime(0); + } + if ($this->_readControl) { + $hashControl = @fread($fp, 32); + $length = $length - 32; + } + if ($length) { + $data = @fread($fp, $length); + } else { + $data = ''; + } + if ($mqr) { + set_magic_quotes_runtime($mqr); + } + if ($this->_fileLocking) @flock($fp, LOCK_UN); + @fclose($fp); + if ($this->_readControl) { + $hashData = $this->_hash($data, $this->_readControlType); + if ($hashData != $hashControl) { + if (!(is_null($this->_lifeTime))) { + @touch($this->_file, time() - 2*abs($this->_lifeTime)); + } else { + @unlink($this->_file); + } + return false; + } + } + return $data; + } + return $this->raiseError('Cache_Lite : Unable to read cache !', -2); + } + + /** + * Write the given data in the cache file + * + * @param string $data data to put in cache + * @return boolean true if ok (a PEAR_Error object else) + * @access private + */ + function _write($data) + { + if ($this->_hashedDirectoryLevel > 0) { + $hash = md5($this->_fileName); + $root = $this->_cacheDir; + for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) { + $root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/'; + if (!(@is_dir($root))) { + @mkdir($root, $this->_hashedDirectoryUmask); + } + } + } + $fp = @fopen($this->_file, "wb"); + if ($fp) { + if ($this->_fileLocking) @flock($fp, LOCK_EX); + if ($this->_readControl) { + @fwrite($fp, $this->_hash($data, $this->_readControlType), 32); + } + $mqr = get_magic_quotes_runtime(); + if ($mqr) { + set_magic_quotes_runtime(0); + } + @fwrite($fp, $data); + if ($mqr) { + set_magic_quotes_runtime($mqr); + } + if ($this->_fileLocking) @flock($fp, LOCK_UN); + @fclose($fp); + return true; + } + return $this->raiseError('Cache_Lite : Unable to write cache file : '.$this->_file, -1); + } + + /** + * Write the given data in the cache file and control it just after to avoir corrupted cache entries + * + * @param string $data data to put in cache + * @return boolean true if the test is ok (else : false or a PEAR_Error object) + * @access private + */ + function _writeAndControl($data) + { + $result = $this->_write($data); + if (is_object($result)) { + return $result; # We return the PEAR_Error object + } + $dataRead = $this->_read(); + if (is_object($dataRead)) { + return $dataRead; # We return the PEAR_Error object + } + if ((is_bool($dataRead)) && (!$dataRead)) { + return false; + } + return ($dataRead==$data); + } + + /** + * Make a control key with the string containing datas + * + * @param string $data data + * @param string $controlType type of control 'md5', 'crc32' or 'strlen' + * @return string control key + * @access private + */ + function _hash($data, $controlType) + { + switch ($controlType) { + case 'md5': + return md5($data); + case 'crc32': + return sprintf('% 32d', crc32($data)); + case 'strlen': + return sprintf('% 32d', strlen($data)); + default: + return $this->raiseError('Unknown controlType ! (available values are only \'md5\', \'crc32\', \'strlen\')', -5); + } + } + +} + +?> diff --git a/Cache/Lite/File.php b/Cache/Lite/File.php new file mode 100644 index 0000000..6913893 --- /dev/null +++ b/Cache/Lite/File.php @@ -0,0 +1,93 @@ + +*/ + +require_once('Cache/Lite.php'); + +class Cache_Lite_File extends Cache_Lite +{ + + // --- Private properties --- + + /** + * Complete path of the file used for controlling the cache lifetime + * + * @var string $_masterFile + */ + var $_masterFile = ''; + + /** + * Masterfile mtime + * + * @var int $_masterFile_mtime + */ + var $_masterFile_mtime = 0; + + // --- Public methods ---- + + /** + * Constructor + * + * $options is an assoc. To have a look at availables options, + * see the constructor of the Cache_Lite class in 'Cache_Lite.php' + * + * Comparing to Cache_Lite constructor, there is another option : + * $options = array( + * (...) see Cache_Lite constructor + * 'masterFile' => complete path of the file used for controlling the cache lifetime(string) + * ); + * + * @param array $options options + * @access public + */ + function Cache_Lite_File($options = array(NULL)) + { + $options['lifetime'] = 0; + $this->Cache_Lite($options); + if (isset($options['masterFile'])) { + $this->_masterFile = $options['masterFile']; + } else { + return $this->raiseError('Cache_Lite_File : masterFile option must be set !'); + } + if (!($this->_masterFile_mtime = @filemtime($this->_masterFile))) { + return $this->raiseError('Cache_Lite_File : Unable to read masterFile : '.$this->_masterFile, -3); + } + } + + /** + * Test if a cache is available and (if yes) return it + * + * @param string $id cache id + * @param string $group name of the cache group + * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested + * @return string data of the cache (else : false) + * @access public + */ + function get($id, $group = 'default', $doNotTestCacheValidity = false) + { + if ($data = parent::get($id, $group, true)) { + if ($filemtime = $this->lastModified()) { + if ($filemtime > $this->_masterFile_mtime) { + return $data; + } + } + } + return false; + } + +} + +?> diff --git a/Cache/Lite/Function.php b/Cache/Lite/Function.php new file mode 100644 index 0000000..af1c5b9 --- /dev/null +++ b/Cache/Lite/Function.php @@ -0,0 +1,211 @@ + +* @author Fabien MARTY +*/ + +require_once('Cache/Lite.php'); + +class Cache_Lite_Function extends Cache_Lite +{ + + // --- Private properties --- + + /** + * Default cache group for function caching + * + * @var string $_defaultGroup + */ + var $_defaultGroup = 'Cache_Lite_Function'; + + /** + * Don't cache the method call when its output contains the string "NOCACHE" + * + * if set to true, the output of the method will never be displayed (because the output is used + * to control the cache) + * + * @var boolean $_dontCacheWhenTheOutputContainsNOCACHE + */ + var $_dontCacheWhenTheOutputContainsNOCACHE = false; + + /** + * Don't cache the method call when its result is false + * + * @var boolean $_dontCacheWhenTheResultIsFalse + */ + var $_dontCacheWhenTheResultIsFalse = false; + + /** + * Don't cache the method call when its result is null + * + * @var boolean $_dontCacheWhenTheResultIsNull + */ + var $_dontCacheWhenTheResultIsNull = false; + + /** + * Debug the Cache_Lite_Function caching process + * + * @var boolean $_debugCacheLiteFunction + */ + var $_debugCacheLiteFunction = false; + + // --- Public methods ---- + + /** + * Constructor + * + * $options is an assoc. To have a look at availables options, + * see the constructor of the Cache_Lite class in 'Cache_Lite.php' + * + * Comparing to Cache_Lite constructor, there is another option : + * $options = array( + * (...) see Cache_Lite constructor + * 'debugCacheLiteFunction' => (bool) debug the caching process, + * 'defaultGroup' => default cache group for function caching (string), + * 'dontCacheWhenTheOutputContainsNOCACHE' => (bool) don't cache when the function output contains "NOCACHE", + * 'dontCacheWhenTheResultIsFalse' => (bool) don't cache when the function result is false, + * 'dontCacheWhenTheResultIsNull' => (bool don't cache when the function result is null + * ); + * + * @param array $options options + * @access public + */ + function Cache_Lite_Function($options = array(NULL)) + { + $availableOptions = array('debugCacheLiteFunction', 'defaultGroup', 'dontCacheWhenTheOutputContainsNOCACHE', 'dontCacheWhenTheResultIsFalse', 'dontCacheWhenTheResultIsNull'); + while (list($name, $value) = each($options)) { + if (in_array($name, $availableOptions)) { + $property = '_'.$name; + $this->$property = $value; + } + } + reset($options); + $this->Cache_Lite($options); + } + + /** + * Calls a cacheable function or method (or not if there is already a cache for it) + * + * Arguments of this method are read with func_get_args. So it doesn't appear + * in the function definition. Synopsis : + * call('functionName', $arg1, $arg2, ...) + * (arg1, arg2... are arguments of 'functionName') + * + * @return mixed result of the function/method + * @access public + */ + function call() + { + $arguments = func_get_args(); + $id = $this->_makeId($arguments); + $data = $this->get($id, $this->_defaultGroup); + if ($data !== false) { + if ($this->_debugCacheLiteFunction) { + echo "Cache hit !\n"; + } + $array = unserialize($data); + $output = $array['output']; + $result = $array['result']; + } else { + if ($this->_debugCacheLiteFunction) { + echo "Cache missed !\n"; + } + ob_start(); + ob_implicit_flush(false); + $target = array_shift($arguments); + if (is_array($target)) { + // in this case, $target is for example array($obj, 'method') + $object = $target[0]; + $method = $target[1]; + $result = call_user_func_array(array(&$object, $method), $arguments); + } else { + if (strstr($target, '::')) { // classname::staticMethod + list($class, $method) = explode('::', $target); + $result = call_user_func_array(array($class, $method), $arguments); + } else if (strstr($target, '->')) { // object->method + // use a stupid name ($objet_123456789 because) of problems where the object + // name is the same as this var name + list($object_123456789, $method) = explode('->', $target); + global $$object_123456789; + $result = call_user_func_array(array($$object_123456789, $method), $arguments); + } else { // function + $result = call_user_func_array($target, $arguments); + } + } + $output = ob_get_contents(); + ob_end_clean(); + if ($this->_dontCacheWhenTheResultIsFalse) { + if ((is_bool($result)) && (!($result))) { + echo($output); + return $result; + } + } + if ($this->_dontCacheWhenTheResultIsNull) { + if (is_null($result)) { + echo($output); + return $result; + } + } + if ($this->_dontCacheWhenTheOutputContainsNOCACHE) { + if (strpos($output, 'NOCACHE') > -1) { + return $result; + } + } + $array['output'] = $output; + $array['result'] = $result; + $this->save(serialize($array), $id, $this->_defaultGroup); + } + echo($output); + return $result; + } + + /** + * Drop a cache file + * + * Arguments of this method are read with func_get_args. So it doesn't appear + * in the function definition. Synopsis : + * remove('functionName', $arg1, $arg2, ...) + * (arg1, arg2... are arguments of 'functionName') + * + * @return boolean true if no problem + * @access public + */ + function drop() + { + $id = $this->_makeId(func_get_args()); + return $this->remove($id, $this->_defaultGroup); + } + + /** + * Make an id for the cache + * + * @var array result of func_get_args for the call() or the remove() method + * @return string id + * @access private + */ + function _makeId($arguments) + { + $id = serialize($arguments); // Generate a cache id + if (!$this->_fileNameProtection) { + $id = md5($id); + // if fileNameProtection is set to false, then the id has to be hashed + // because it's a very bad file name in most cases + } + return $id; + } + +} + +?> diff --git a/Cache/Lite/Output.php b/Cache/Lite/Output.php new file mode 100644 index 0000000..6dd24d5 --- /dev/null +++ b/Cache/Lite/Output.php @@ -0,0 +1,72 @@ + +*/ + +require_once('Cache/Lite.php'); + +class Cache_Lite_Output extends Cache_Lite +{ + + // --- Public methods --- + + /** + * Constructor + * + * $options is an assoc. To have a look at availables options, + * see the constructor of the Cache_Lite class in 'Cache_Lite.php' + * + * @param array $options options + * @access public + */ + function Cache_Lite_Output($options) + { + $this->Cache_Lite($options); + } + + /** + * Start the cache + * + * @param string $id cache id + * @param string $group name of the cache group + * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested + * @return boolean true if the cache is hit (false else) + * @access public + */ + function start($id, $group = 'default', $doNotTestCacheValidity = false) + { + $data = $this->get($id, $group, $doNotTestCacheValidity); + if ($data !== false) { + echo($data); + return true; + } + ob_start(); + ob_implicit_flush(false); + return false; + } + + /** + * Stop the cache + * + * @access public + */ + function end() + { + $data = ob_get_contents(); + ob_end_clean(); + $this->save($data, $this->_id, $this->_group); + echo($data); + } + +} + + +?> diff --git a/Calendar/Calendar.php b/Calendar/Calendar.php new file mode 100644 index 0000000..e1025dd --- /dev/null +++ b/Calendar/Calendar.php @@ -0,0 +1,685 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Calendar.php,v 1.3 2005/10/22 10:07:11 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Calendar.php,v 1.3 2005/10/22 10:07:11 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Constant which defines the calculation engine to use + */ +if (!defined('CALENDAR_ENGINE')) { + define('CALENDAR_ENGINE', 'UnixTS'); +} + +/** + * Define Calendar Month states + */ +define('CALENDAR_USE_MONTH', 1); +define('CALENDAR_USE_MONTH_WEEKDAYS', 2); +define('CALENDAR_USE_MONTH_WEEKS', 3); + +/** + * Contains a factory method to return a Singleton instance of a class + * implementing the Calendar_Engine_Interface.
+ * Note: this class must be modified to "register" alternative + * Calendar_Engines. The engine used can be controlled with the constant + * CALENDAR_ENGINE + * @see Calendar_Engine_Interface + * @package Calendar + * @access protected + */ +class Calendar_Engine_Factory +{ + /** + * Returns an instance of the engine + * @return object instance of a calendar calculation engine + * @access protected + */ + function & getEngine() + { + static $engine = false; + switch (CALENDAR_ENGINE) { + case 'PearDate': + $class = 'Calendar_Engine_PearDate'; + break; + case 'UnixTS': + default: + $class = 'Calendar_Engine_UnixTS'; + break; + } + if (!$engine) { + if (!class_exists($class)) { + require_once CALENDAR_ROOT.'Engine'.DIRECTORY_SEPARATOR.CALENDAR_ENGINE.'.php'; + } + $engine = new $class; + } + return $engine; + } +} + +/** + * Base class for Calendar API. This class should not be instantiated + * directly. + * @abstract + * @package Calendar + */ +class Calendar +{ + /** + * Instance of class implementing calendar engine interface + * @var object + * @access private + */ + var $cE; + + /** + * Instance of Calendar_Validator (lazy initialized when isValid() or + * getValidor() is called + * @var Calendar_Validator + * @access private + */ + var $validator; + + /** + * Year for this calendar object e.g. 2003 + * @access private + * @var int + */ + var $year; + + /** + * Month for this calendar object e.g. 9 + * @access private + * @var int + */ + var $month; + + /** + * Day of month for this calendar object e.g. 23 + * @access private + * @var int + */ + var $day; + + /** + * Hour of day for this calendar object e.g. 13 + * @access private + * @var int + */ + var $hour; + + /** + * Minute of hour this calendar object e.g. 46 + * @access private + * @var int + */ + var $minute; + + /** + * Second of minute this calendar object e.g. 34 + * @access private + * @var int + */ + var $second; + + /** + * Marks this calendar object as selected (e.g. 'today') + * @access private + * @var boolean + */ + var $selected = false; + + /** + * Collection of child calendar objects created from subclasses + * of Calendar. Type depends on the object which created them. + * @access private + * @var array + */ + var $children = array(); + + /** + * Constructs the Calendar + * @param int year + * @param int month + * @param int day + * @param int hour + * @param int minute + * @param int second + * @access protected + */ + function Calendar($y = 2000, $m = 1, $d = 1, $h = 0, $i = 0, $s = 0) + { + static $cE = null; + if (!isset($cE)) { + $cE = & Calendar_Engine_Factory::getEngine(); + } + $this->cE = & $cE; + $this->year = (int)$y; + $this->month = (int)$m; + $this->day = (int)$d; + $this->hour = (int)$h; + $this->minute = (int)$i; + $this->second = (int)$s; + } + + /** + * Defines the calendar by a timestamp (Unix or ISO-8601), replacing values + * passed to the constructor + * @param int|string Unix or ISO-8601 timestamp + * @return void + * @access public + */ + function setTimestamp($ts) + { + $this->year = $this->cE->stampToYear($ts); + $this->month = $this->cE->stampToMonth($ts); + $this->day = $this->cE->stampToDay($ts); + $this->hour = $this->cE->stampToHour($ts); + $this->minute = $this->cE->stampToMinute($ts); + $this->second = $this->cE->stampToSecond($ts); + } + + /** + * Returns a timestamp from the current date / time values. Format of + * timestamp depends on Calendar_Engine implementation being used + * @return int|string timestamp + * @access public + */ + function getTimestamp() + { + return $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute, $this->second); + } + + /** + * Defines calendar object as selected (e.g. for today) + * @param boolean state whether Calendar subclass + * @return void + * @access public + */ + function setSelected($state = true) + { + $this->selected = $state; + } + + /** + * True if the calendar subclass object is selected (e.g. today) + * @return boolean + * @access public + */ + function isSelected() + { + return $this->selected; + } + + /** + * Adjusts the date (helper method) + * @return void + * @access public + */ + function adjust() + { + $stamp = $this->getTimeStamp(); + $this->year = $this->cE->stampToYear($stamp); + $this->month = $this->cE->stampToMonth($stamp); + $this->day = $this->cE->stampToDay($stamp); + $this->hour = $this->cE->stampToHour($stamp); + $this->minute = $this->cE->stampToMinute($stamp); + $this->second = $this->cE->stampToSecond($stamp); + } + + /** + * Returns the date as an associative array (helper method) + * @param mixed timestamp (leave empty for current timestamp) + * @return array + * @access public + */ + function toArray($stamp=null) + { + if (is_null($stamp)) { + $stamp = $this->getTimeStamp(); + } + return array( + 'year' => $this->cE->stampToYear($stamp), + 'month' => $this->cE->stampToMonth($stamp), + 'day' => $this->cE->stampToDay($stamp), + 'hour' => $this->cE->stampToHour($stamp), + 'minute' => $this->cE->stampToMinute($stamp), + 'second' => $this->cE->stampToSecond($stamp) + ); + } + + /** + * Returns the value as an associative array (helper method) + * @param string type of date object that return value represents + * @param string $format ['int' | 'array' | 'timestamp' | 'object'] + * @param mixed timestamp (depending on Calendar engine being used) + * @param int integer default value (i.e. give me the answer quick) + * @return mixed + * @access private + */ + function returnValue($returnType, $format, $stamp, $default) + { + switch (strtolower($format)) { + case 'int': + return $default; + case 'array': + return $this->toArray($stamp); + break; + case 'object': + require_once CALENDAR_ROOT.'Factory.php'; + return Calendar_Factory::createByTimestamp($returnType,$stamp); + break; + case 'timestamp': + default: + return $stamp; + break; + } + } + + /** + * Abstract method for building the children of a calendar object. + * Implemented by Calendar subclasses + * @param array containing Calendar objects to select (optional) + * @return boolean + * @access public + * @abstract + */ + function build($sDates = array()) + { + require_once 'PEAR.php'; + PEAR::raiseError( + 'Calendar::build is abstract', null, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar::build()'); + return false; + } + + /** + * Abstract method for selected data objects called from build + * @param array + * @return boolean + * @access public + * @abstract + */ + function setSelection($sDates) + { + require_once 'PEAR.php'; + PEAR::raiseError( + 'Calendar::setSelection is abstract', null, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar::setSelection()'); + return false; + } + + /** + * Iterator method for fetching child Calendar subclass objects + * (e.g. a minute from an hour object). On reaching the end of + * the collection, returns false and resets the collection for + * further iteratations. + * @return mixed either an object subclass of Calendar or false + * @access public + */ + function fetch() + { + $child = each($this->children); + if ($child) { + return $child['value']; + } else { + reset($this->children); + return false; + } + } + + /** + * Fetches all child from the current collection of children + * @return array + * @access public + */ + function fetchAll() + { + return $this->children; + } + + /** + * Get the number Calendar subclass objects stored in the internal + * collection. + * @return int + * @access public + */ + function size() + { + return count($this->children); + } + + /** + * Determine whether this date is valid, with the bounds determined by + * the Calendar_Engine. The call is passed on to + * Calendar_Validator::isValid + * @return boolean + * @access public + */ + function isValid() + { + $validator = & $this->getValidator(); + return $validator->isValid(); + } + + /** + * Returns an instance of Calendar_Validator + * @return Calendar_Validator + * @access public + */ + function & getValidator() + { + if (!isset($this->validator)) { + require_once CALENDAR_ROOT.'Validator.php'; + $this->validator = & new Calendar_Validator($this); + } + return $this->validator; + } + + /** + * Returns a reference to the current Calendar_Engine being used. Useful + * for Calendar_Table_Helper and Calendar_Validator + * @return object implementing Calendar_Engine_Inteface + * @access protected + */ + function & getEngine() + { + return $this->cE; + } + + /** + * Set the CALENDAR_FIRST_DAY_OF_WEEK constant to the $firstDay value + * if the constant is not set yet. + * @throws E_USER_WARNING this method throws a WARNING if the + * CALENDAR_FIRST_DAY_OF_WEEK constant is already defined and + * the $firstDay parameter is set to a different value + * @param integer $firstDay first day of the week (0=sunday, 1=monday, ...) + * @return integer + * @access protected + */ + function defineFirstDayOfWeek($firstDay = null) + { + if (defined('CALENDAR_FIRST_DAY_OF_WEEK')) { + if (!is_null($firstDay) && ($firstDay != CALENDAR_FIRST_DAY_OF_WEEK)) { + $msg = 'CALENDAR_FIRST_DAY_OF_WEEK constant already defined.' + .' The $firstDay parameter will be ignored.'; + trigger_error($msg, E_USER_WARNING); + } + return CALENDAR_FIRST_DAY_OF_WEEK; + } + if (is_null($firstDay)) { + $firstDay = $this->cE->getFirstDayOfWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay() + ); + } + define ('CALENDAR_FIRST_DAY_OF_WEEK', $firstDay); + return CALENDAR_FIRST_DAY_OF_WEEK; + } + + /** + * Returns the value for the previous year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2002 or timestamp + * @access public + */ + function prevYear($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year-1, 1, 1, 0, 0, 0); + return $this->returnValue('Year', $format, $ts, $this->year-1); + } + + /** + * Returns the value for this year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2003 or timestamp + * @access public + */ + function thisYear($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year, 1, 1, 0, 0, 0); + return $this->returnValue('Year', $format, $ts, $this->year); + } + + /** + * Returns the value for next year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2004 or timestamp + * @access public + */ + function nextYear($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year+1, 1, 1, 0, 0, 0); + return $this->returnValue('Year', $format, $ts, $this->year+1); + } + + /** + * Returns the value for the previous month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 4 or Unix timestamp + * @access public + */ + function prevMonth($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year, $this->month-1, 1, 0, 0, 0); + return $this->returnValue('Month', $format, $ts, $this->cE->stampToMonth($ts)); + } + + /** + * Returns the value for this month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 5 or timestamp + * @access public + */ + function thisMonth($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year, $this->month, 1, 0, 0, 0); + return $this->returnValue('Month', $format, $ts, $this->month); + } + + /** + * Returns the value for next month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 6 or timestamp + * @access public + */ + function nextMonth($format = 'int') + { + $ts = $this->cE->dateToStamp($this->year, $this->month+1, 1, 0, 0, 0); + return $this->returnValue('Month', $format, $ts, $this->cE->stampToMonth($ts)); + } + + /** + * Returns the value for the previous day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 10 or timestamp + * @access public + */ + function prevDay($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day-1, 0, 0, 0); + return $this->returnValue('Day', $format, $ts, $this->cE->stampToDay($ts)); + } + + /** + * Returns the value for this day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 11 or timestamp + * @access public + */ + function thisDay($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, 0, 0, 0); + return $this->returnValue('Day', $format, $ts, $this->day); + } + + /** + * Returns the value for the next day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 12 or timestamp + * @access public + */ + function nextDay($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day+1, 0, 0, 0); + return $this->returnValue('Day', $format, $ts, $this->cE->stampToDay($ts)); + } + + /** + * Returns the value for the previous hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 13 or timestamp + * @access public + */ + function prevHour($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, $this->hour-1, 0, 0); + return $this->returnValue('Hour', $format, $ts, $this->cE->stampToHour($ts)); + } + + /** + * Returns the value for this hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 14 or timestamp + * @access public + */ + function thisHour($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, $this->hour, 0, 0); + return $this->returnValue('Hour', $format, $ts, $this->hour); + } + + /** + * Returns the value for the next hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 14 or timestamp + * @access public + */ + function nextHour($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, $this->hour+1, 0, 0); + return $this->returnValue('Hour', $format, $ts, $this->cE->stampToHour($ts)); + } + + /** + * Returns the value for the previous minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 23 or timestamp + * @access public + */ + function prevMinute($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute-1, 0); + return $this->returnValue('Minute', $format, $ts, $this->cE->stampToMinute($ts)); + } + + /** + * Returns the value for this minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 24 or timestamp + * @access public + */ + function thisMinute($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute, 0); + return $this->returnValue('Minute', $format, $ts, $this->minute); + } + + /** + * Returns the value for the next minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 25 or timestamp + * @access public + */ + function nextMinute($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute+1, 0); + return $this->returnValue('Minute', $format, $ts, $this->cE->stampToMinute($ts)); + } + + /** + * Returns the value for the previous second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 43 or timestamp + * @access public + */ + function prevSecond($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute, $this->second-1); + return $this->returnValue('Second', $format, $ts, $this->cE->stampToSecond($ts)); + } + + /** + * Returns the value for this second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 44 or timestamp + * @access public + */ + function thisSecond($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute, $this->second); + return $this->returnValue('Second', $format, $ts, $this->second); + } + + /** + * Returns the value for the next second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 45 or timestamp + * @access public + */ + function nextSecond($format = 'int') + { + $ts = $this->cE->dateToStamp( + $this->year, $this->month, $this->day, + $this->hour, $this->minute, $this->second+1); + return $this->returnValue('Second', $format, $ts, $this->cE->stampToSecond($ts)); + } +} +?> \ No newline at end of file diff --git a/Calendar/Day.php b/Calendar/Day.php new file mode 100644 index 0000000..ffa382f --- /dev/null +++ b/Calendar/Day.php @@ -0,0 +1,197 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Day.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Day.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Day and builds Hours. + * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Day.php'; + * $Day = & new Calendar_Day(2003, 10, 21); // Oct 21st 2003 + * while ($Hour = & $Day->fetch()) { + * echo $Hour->thisHour().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Day extends Calendar +{ + /** + * Marks the Day at the beginning of a week + * @access private + * @var boolean + */ + var $first = false; + + /** + * Marks the Day at the end of a week + * @access private + * @var boolean + */ + var $last = false; + + + /** + * Used for tabular calendars + * @access private + * @var boolean + */ + var $empty = false; + + /** + * Constructs Calendar_Day + * @param int year e.g. 2003 + * @param int month e.g. 8 + * @param int day e.g. 15 + * @access public + */ + function Calendar_Day($y, $m, $d) + { + Calendar::Calendar($y, $m, $d); + } + + /** + * Builds the Hours of the Day + * @param array (optional) Caledar_Hour objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates = array()) + { + require_once CALENDAR_ROOT.'Hour.php'; + + $hID = $this->cE->getHoursInDay($this->year, $this->month, $this->day); + for ($i=0; $i < $hID; $i++) { + $this->children[$i]= + new Calendar_Hour($this->year, $this->month, $this->day, $i); + } + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear() + && $this->month == $sDate->thisMonth() + && $this->day == $sDate->thisDay()) + { + $key = (int)$sDate->thisHour(); + if (isset($this->children[$key])) { + $sDate->setSelected(); + $this->children[$key] = $sDate; + } + } + } + } + + /** + * Defines Day object as first in a week + * Only used by Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setFirst ($state = true) + { + $this->first = $state; + } + + /** + * Defines Day object as last in a week + * Used only following Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setLast($state = true) + { + $this->last = $state; + } + + /** + * Returns true if Day object is first in a Week + * Only relevant when Day is created by Calendar_Month_Weekdays::build() + * @return boolean + * @access public + */ + function isFirst() { + return $this->first; + } + + /** + * Returns true if Day object is last in a Week + * Only relevant when Day is created by Calendar_Month_Weekdays::build() + * @return boolean + * @access public + */ + function isLast() + { + return $this->last; + } + + /** + * Defines Day object as empty + * Only used by Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setEmpty ($state = true) + { + $this->empty = $state; + } + + /** + * @return boolean + * @access public + */ + function isEmpty() + { + return $this->empty; + } +} +?> \ No newline at end of file diff --git a/Calendar/Decorator.php b/Calendar/Decorator.php new file mode 100644 index 0000000..8f409b8 --- /dev/null +++ b/Calendar/Decorator.php @@ -0,0 +1,558 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Decorator.php,v 1.3 2005/10/22 10:29:46 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Decorator.php,v 1.3 2005/10/22 10:29:46 quipo Exp $ + */ +/** + * Decorates any calendar class. + * Create a subclass of this class for your own "decoration". + * Used for "selections" + * + * class DayDecorator extends Calendar_Decorator + * { + * function thisDay($format = 'int') + * { +.* $day = parent::thisDay('timestamp'); +.* return date('D', $day); + * } + * } + * $Day = & new Calendar_Day(2003, 10, 25); + * $DayDecorator = & new DayDecorator($Day); + * echo $DayDecorator->thisDay(); // Outputs "Sat" + * + * @abstract + * @package Calendar + */ +class Calendar_Decorator +{ + /** + * Subclass of Calendar being decorated + * @var object + * @access private + */ + var $calendar; + + /** + * Constructs the Calendar_Decorator + * @param object subclass to Calendar to decorate + */ + function Calendar_Decorator(& $calendar) + { + $this->calendar = & $calendar; + } + + /** + * Defines the calendar by a Unix timestamp, replacing values + * passed to the constructor + * @param int Unix timestamp + * @return void + * @access public + */ + function setTimestamp($ts) + { + $this->calendar->setTimestamp($ts); + } + + /** + * Returns a timestamp from the current date / time values. Format of + * timestamp depends on Calendar_Engine implementation being used + * @return int timestamp + * @access public + */ + function getTimestamp() + { + return $this->calendar->getTimeStamp(); + } + + /** + * Defines calendar object as selected (e.g. for today) + * @param boolean state whether Calendar subclass + * @return void + * @access public + */ + function setSelected($state = true) + { + $this->calendar->setSelected($state = true); + } + + /** + * True if the calendar subclass object is selected (e.g. today) + * @return boolean + * @access public + */ + function isSelected() + { + return $this->calendar->isSelected(); + } + + /** + * Adjusts the date (helper method) + * @return void + * @access public + */ + function adjust() + { + $this->calendar->adjust(); + } + + /** + * Returns the date as an associative array (helper method) + * @param mixed timestamp (leave empty for current timestamp) + * @return array + * @access public + */ + function toArray($stamp=null) + { + return $this->calendar->toArray($stamp); + } + + /** + * Returns the value as an associative array (helper method) + * @param string type of date object that return value represents + * @param string $format ['int' | 'array' | 'timestamp' | 'object'] + * @param mixed timestamp (depending on Calendar engine being used) + * @param int integer default value (i.e. give me the answer quick) + * @return mixed + * @access private + */ + function returnValue($returnType, $format, $stamp, $default) + { + return $this->calendar->returnValue($returnType, $format, $stamp, $default); + } + + /** + * Defines Day object as first in a week + * Only used by Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setFirst ($state = true) + { + if ( method_exists($this->calendar,'setFirst') ) { + $this->calendar->setFirst($state); + } + } + + /** + * Defines Day object as last in a week + * Used only following Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setLast($state = true) + { + if ( method_exists($this->calendar,'setLast') ) { + $this->calendar->setLast($state); + } + } + + /** + * Returns true if Day object is first in a Week + * Only relevant when Day is created by Calendar_Month_Weekdays::build() + * @return boolean + * @access public + */ + function isFirst() { + if ( method_exists($this->calendar,'isFirst') ) { + return $this->calendar->isFirst(); + } + } + + /** + * Returns true if Day object is last in a Week + * Only relevant when Day is created by Calendar_Month_Weekdays::build() + * @return boolean + * @access public + */ + function isLast() + { + if ( method_exists($this->calendar,'isLast') ) { + return $this->calendar->isLast(); + } + } + + /** + * Defines Day object as empty + * Only used by Calendar_Month_Weekdays::build() + * @param boolean state + * @return void + * @access private + */ + function setEmpty ($state = true) + { + if ( method_exists($this->calendar,'setEmpty') ) { + $this->calendar->setEmpty($state); + } + } + + /** + * @return boolean + * @access public + */ + function isEmpty() + { + if ( method_exists($this->calendar,'isEmpty') ) { + return $this->calendar->isEmpty(); + } + } + + /** + * Build the children + * @param array containing Calendar objects to select (optional) + * @return boolean + * @access public + * @abstract + */ + function build($sDates = array()) + { + $this->calendar->build($sDates); + } + + /** + * Iterator method for fetching child Calendar subclass objects + * (e.g. a minute from an hour object). On reaching the end of + * the collection, returns false and resets the collection for + * further iteratations. + * @return mixed either an object subclass of Calendar or false + * @access public + */ + function fetch() + { + return $this->calendar->fetch(); + } + + /** + * Fetches all child from the current collection of children + * @return array + * @access public + */ + function fetchAll() + { + return $this->calendar->fetchAll(); + } + + /** + * Get the number Calendar subclass objects stored in the internal + * collection. + * @return int + * @access public + */ + function size() + { + return $this->calendar->size(); + } + + /** + * Determine whether this date is valid, with the bounds determined by + * the Calendar_Engine. The call is passed on to + * Calendar_Validator::isValid + * @return boolean + * @access public + */ + function isValid() + { + return $this->calendar->isValid(); + } + + /** + * Returns an instance of Calendar_Validator + * @return Calendar_Validator + * @access public + */ + function & getValidator() + { + $validator = $this->calendar->getValidator(); + return $validator; + } + + /** + * Returns a reference to the current Calendar_Engine being used. Useful + * for Calendar_Table_Helper and Calendar_Validator + * @return object implementing Calendar_Engine_Inteface + * @access private + */ + function & getEngine() + { + return $this->calendar->getEngine(); + } + + /** + * Returns the value for the previous year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2002 or timestamp + * @access public + */ + function prevYear($format = 'int') + { + return $this->calendar->prevYear($format); + } + + /** + * Returns the value for this year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2003 or timestamp + * @access public + */ + function thisYear($format = 'int') + { + return $this->calendar->thisYear($format); + } + + /** + * Returns the value for next year + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 2004 or timestamp + * @access public + */ + function nextYear($format = 'int') + { + return $this->calendar->nextYear($format); + } + + /** + * Returns the value for the previous month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 4 or Unix timestamp + * @access public + */ + function prevMonth($format = 'int') + { + return $this->calendar->prevMonth($format); + } + + /** + * Returns the value for this month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 5 or timestamp + * @access public + */ + function thisMonth($format = 'int') + { + return $this->calendar->thisMonth($format); + } + + /** + * Returns the value for next month + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 6 or timestamp + * @access public + */ + function nextMonth($format = 'int') + { + return $this->calendar->nextMonth($format); + } + + /** + * Returns the value for the previous week + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 4 or Unix timestamp + * @access public + */ + function prevWeek($format = 'n_in_month') + { + if ( method_exists($this->calendar,'prevWeek') ) { + return $this->calendar->prevWeek($format); + } else { + require_once 'PEAR.php'; + PEAR::raiseError( + 'Cannot call prevWeek on Calendar object of type: '. + get_class($this->calendar), 133, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar_Decorator::prevWeek()'); + return false; + } + } + + /** + * Returns the value for this week + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 5 or timestamp + * @access public + */ + function thisWeek($format = 'n_in_month') + { + if ( method_exists($this->calendar,'thisWeek') ) { + return $this->calendar->thisWeek($format); + } else { + require_once 'PEAR.php'; + PEAR::raiseError( + 'Cannot call thisWeek on Calendar object of type: '. + get_class($this->calendar), 133, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar_Decorator::thisWeek()'); + return false; + } + } + + /** + * Returns the value for next week + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 6 or timestamp + * @access public + */ + function nextWeek($format = 'n_in_month') + { + if ( method_exists($this->calendar,'nextWeek') ) { + return $this->calendar->nextWeek($format); + } else { + require_once 'PEAR.php'; + PEAR::raiseError( + 'Cannot call thisWeek on Calendar object of type: '. + get_class($this->calendar), 133, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar_Decorator::nextWeek()'); + return false; + } + } + + /** + * Returns the value for the previous day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 10 or timestamp + * @access public + */ + function prevDay($format = 'int') { + return $this->calendar->prevDay($format); + } + + /** + * Returns the value for this day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 11 or timestamp + * @access public + */ + function thisDay($format = 'int') + { + return $this->calendar->thisDay($format); + } + + /** + * Returns the value for the next day + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 12 or timestamp + * @access public + */ + function nextDay($format = 'int') + { + return $this->calendar->nextDay($format); + } + + /** + * Returns the value for the previous hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 13 or timestamp + * @access public + */ + function prevHour($format = 'int') + { + return $this->calendar->prevHour($format); + } + + /** + * Returns the value for this hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 14 or timestamp + * @access public + */ + function thisHour($format = 'int') + { + return $this->calendar->thisHour($format); + } + + /** + * Returns the value for the next hour + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 14 or timestamp + * @access public + */ + function nextHour($format = 'int') + { + return $this->calendar->nextHour($format); + } + + /** + * Returns the value for the previous minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 23 or timestamp + * @access public + */ + function prevMinute($format = 'int') + { + return $this->calendar->prevMinute($format); + } + + /** + * Returns the value for this minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 24 or timestamp + * @access public + */ + function thisMinute($format = 'int') + { + return $this->calendar->thisMinute($format); + } + + /** + * Returns the value for the next minute + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 25 or timestamp + * @access public + */ + function nextMinute($format = 'int') + { + return $this->calendar->nextMinute($format); + } + + /** + * Returns the value for the previous second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 43 or timestamp + * @access public + */ + function prevSecond($format = 'int') + { + return $this->calendar->prevSecond($format); + } + + /** + * Returns the value for this second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 44 or timestamp + * @access public + */ + function thisSecond($format = 'int') + { + return $this->calendar->thisSecond($format); + } + + /** + * Returns the value for the next second + * @param string return value format ['int' | 'timestamp' | 'object' | 'array'] + * @return int e.g. 45 or timestamp + * @access public + */ + function nextSecond($format = 'int') + { + return $this->calendar->nextSecond($format); + } +} +?> \ No newline at end of file diff --git a/Calendar/Decorator/Textual.php b/Calendar/Decorator/Textual.php new file mode 100644 index 0000000..08ada8a --- /dev/null +++ b/Calendar/Decorator/Textual.php @@ -0,0 +1,169 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Textual.php,v 1.3 2004/08/16 13:02:44 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Textual.php,v 1.3 2004/08/16 13:02:44 hfuecks Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar decorator base class + */ +require_once CALENDAR_ROOT.'Decorator.php'; + +/** + * Load the Uri utility + */ +require_once CALENDAR_ROOT.'Util'.DIRECTORY_SEPARATOR.'Textual.php'; + +/** + * Decorator to help with fetching textual representations of months and + * days of the week. + * Note: for performance you should prefer Calendar_Util_Textual unless you + * have a specific need to use a decorator + * @package Calendar + * @access public + */ +class Calendar_Decorator_Textual extends Calendar_Decorator +{ + /** + * Constructs Calendar_Decorator_Textual + * @param object subclass of Calendar + * @access public + */ + function Calendar_Decorator_Textual(&$Calendar) + { + parent::Calendar_Decorator($Calendar); + } + + /** + * Returns an array of 12 month names (first index = 1) + * @param string (optional) format of returned months (one,two,short or long) + * @return array + * @access public + * @static + */ + function monthNames($format='long') + { + return Calendar_Util_Textual::monthNames($format); + } + + /** + * Returns an array of 7 week day names (first index = 0) + * @param string (optional) format of returned days (one,two,short or long) + * @return array + * @access public + * @static + */ + function weekdayNames($format='long') + { + return Calendar_Util_Textual::weekdayNames($format); + } + + /** + * Returns textual representation of the previous month of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function prevMonthName($format='long') + { + return Calendar_Util_Textual::prevMonthName($this->calendar,$format); + } + + /** + * Returns textual representation of the month of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function thisMonthName($format='long') + { + return Calendar_Util_Textual::thisMonthName($this->calendar,$format); + } + + /** + * Returns textual representation of the next month of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function nextMonthName($format='long') + { + return Calendar_Util_Textual::nextMonthName($this->calendar,$format); + } + + /** + * Returns textual representation of the previous day of week of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function prevDayName($format='long') + { + return Calendar_Util_Textual::prevDayName($this->calendar,$format); + } + + /** + * Returns textual representation of the day of week of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function thisDayName($format='long') + { + return Calendar_Util_Textual::thisDayName($this->calendar,$format); + } + + /** + * Returns textual representation of the next day of week of the decorated calendar object + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + */ + function nextDayName($format='long') + { + return Calendar_Util_Textual::nextDayName($this->calendar,$format); + } + + /** + * Returns the days of the week using the order defined in the decorated + * calendar object. Only useful for Calendar_Month_Weekdays, Calendar_Month_Weeks + * and Calendar_Week. Otherwise the returned array will begin on Sunday + * @param string (optional) format of returned months (one,two,short or long) + * @return array ordered array of week day names + * @access public + */ + function orderedWeekdays($format='long') + { + return Calendar_Util_Textual::orderedWeekdays($this->calendar,$format); + } +} +?> \ No newline at end of file diff --git a/Calendar/Decorator/Uri.php b/Calendar/Decorator/Uri.php new file mode 100644 index 0000000..9a73e6e --- /dev/null +++ b/Calendar/Decorator/Uri.php @@ -0,0 +1,151 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Uri.php,v 1.3 2004/08/16 09:04:20 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Uri.php,v 1.3 2004/08/16 09:04:20 hfuecks Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar decorator base class + */ +require_once CALENDAR_ROOT.'Decorator.php'; + +/** + * Load the Uri utility + */ +require_once CALENDAR_ROOT.'Util'.DIRECTORY_SEPARATOR.'Uri.php'; + +/** + * Decorator to help with building HTML links for navigating the calendar
+ * Note: for performance you should prefer Calendar_Util_Uri unless you + * have a specific need to use a decorator + * + * $Day = new Calendar_Day(2003, 10, 23); + * $Uri = & new Calendar_Decorator_Uri($Day); + * $Uri->setFragments('year', 'month', 'day'); + * echo $Uri->getPrev(); // Displays year=2003&month=10&day=22 + * + * @see Calendar_Util_Uri + * @package Calendar + * @access public + */ +class Calendar_Decorator_Uri extends Calendar_Decorator +{ + + /** + * @var Calendar_Util_Uri + * @access private + */ + var $Uri; + + /** + * Constructs Calendar_Decorator_Uri + * @param object subclass of Calendar + * @access public + */ + function Calendar_Decorator_Uri(&$Calendar) + { + parent::Calendar_Decorator($Calendar); + } + + /** + * Sets the URI fragment names + * @param string URI fragment for year + * @param string (optional) URI fragment for month + * @param string (optional) URI fragment for day + * @param string (optional) URI fragment for hour + * @param string (optional) URI fragment for minute + * @param string (optional) URI fragment for second + * @return void + * @access public + */ + function setFragments($y, $m=null, $d=null, $h=null, $i=null, $s=null) { + $this->Uri = & new Calendar_Util_Uri($y, $m, $d, $h, $i, $s); + } + + /** + * Sets the separator string between fragments + * @param string separator e.g. / + * @return void + * @access public + */ + function setSeparator($separator) + { + $this->Uri->separator = $separator; + } + + /** + * Puts Uri decorator into "scalar mode" - URI variable names are not + * returned + * @param boolean (optional) + * @return void + * @access public + */ + function setScalar($state=true) + { + $this->Uri->scalar = $state; + } + + /** + * Gets the URI string for the previous calendar unit + * @param string calendar unit to fetch uri for (year,month,week or day etc) + * @return string + * @access public + */ + function prev($method) + { + return $this->Uri->prev($this, $method); + } + + /** + * Gets the URI string for the current calendar unit + * @param string calendar unit to fetch uri for (year,month,week or day etc) + * @return string + * @access public + */ + function this($method) + { + return $this->Uri->this($this, $method); + } + + /** + * Gets the URI string for the next calendar unit + * @param string calendar unit to fetch uri for (year,month,week or day etc) + * @return string + * @access public + */ + function next($method) + { + return $this->Uri->next($this, $method); + } + +} +?> \ No newline at end of file diff --git a/Calendar/Decorator/Weekday.php b/Calendar/Decorator/Weekday.php new file mode 100644 index 0000000..922ab13 --- /dev/null +++ b/Calendar/Decorator/Weekday.php @@ -0,0 +1,148 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Weekday.php,v 1.3 2004/08/16 12:25:15 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Weekday.php,v 1.3 2004/08/16 12:25:15 hfuecks Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar decorator base class + */ +require_once CALENDAR_ROOT.'Decorator.php'; + +/** + * Load a Calendar_Day + */ +require_once CALENDAR_ROOT.'Day.php'; +/** + * Decorator for fetching the day of the week + * + * $Day = new Calendar_Day(2003, 10, 23); + * $Weekday = & new Calendar_Decorator_Weekday($Day); + * $Weekday->setFirstDay(0); // Set first day of week to Sunday (default Mon) + * echo $Weekday->thisWeekDay(); // Displays 5 - fifth day of week relative to Sun + * + * @package Calendar + * @access public + */ +class Calendar_Decorator_Weekday extends Calendar_Decorator +{ + /** + * First day of week + * @var int (default = 1 for Monday) + * @access private + */ + var $firstDay = 1; + + /** + * Constructs Calendar_Decorator_Weekday + * @param object subclass of Calendar + * @access public + */ + function Calendar_Decorator_Weekday(& $Calendar) + { + parent::Calendar_Decorator($Calendar); + } + + /** + * Sets the first day of the week (0 = Sunday, 1 = Monday (default) etc) + * @param int first day of week + * @return void + * @access public + */ + function setFirstDay($firstDay) { + $this->firstDay = (int)$firstDay; + } + + /** + * Returns the previous weekday + * @param string (default = 'int') return value format + * @return int numeric day of week or timestamp + * @access public + */ + function prevWeekDay($format = 'int') + { + $ts = $this->calendar->prevDay('timestamp'); + $Day = new Calendar_Day(2000,1,1); + $Day->setTimeStamp($ts); + $day = $this->calendar->cE->getDayOfWeek($Day->thisYear(),$Day->thisMonth(),$Day->thisDay()); + $day = $this->adjustWeekScale($day); + return $this->returnValue('Day', $format, $ts, $day); + } + + /** + * Returns the current weekday + * @param string (default = 'int') return value format + * @return int numeric day of week or timestamp + * @access public + */ + function thisWeekDay($format = 'int') + { + $ts = $this->calendar->thisDay('timestamp'); + $day = $this->calendar->cE->getDayOfWeek($this->calendar->year,$this->calendar->month,$this->calendar->day); + $day = $this->adjustWeekScale($day); + return $this->returnValue('Day', $format, $ts, $day); + } + + /** + * Returns the next weekday + * @param string (default = 'int') return value format + * @return int numeric day of week or timestamp + * @access public + */ + function nextWeekDay($format = 'int') + { + $ts = $this->calendar->nextDay('timestamp'); + $Day = new Calendar_Day(2000,1,1); + $Day->setTimeStamp($ts); + $day = $this->calendar->cE->getDayOfWeek($Day->thisYear(),$Day->thisMonth(),$Day->thisDay()); + $day = $this->adjustWeekScale($day); + return $this->returnValue('Day', $format, $ts, $day); + } + + /** + * Adjusts the day of the week relative to the first day of the week + * @param int day of week calendar from Calendar_Engine + * @return int day of week adjusted to first day + * @access private + */ + function adjustWeekScale($dayOfWeek) { + $dayOfWeek = $dayOfWeek - $this->firstDay; + if ( $dayOfWeek >= 0 ) { + return $dayOfWeek; + } else { + return $this->calendar->cE->getDaysInWeek( + $this->calendar->year,$this->calendar->month,$this->calendar->day + ) + $dayOfWeek; + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Decorator/Wrapper.php b/Calendar/Decorator/Wrapper.php new file mode 100644 index 0000000..13eaf78 --- /dev/null +++ b/Calendar/Decorator/Wrapper.php @@ -0,0 +1,90 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Wrapper.php,v 1.2 2005/11/03 20:35:03 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Wrapper.php,v 1.2 2005/11/03 20:35:03 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar decorator base class + */ +require_once CALENDAR_ROOT.'Decorator.php'; + +/** + * Decorator to help with wrapping built children in another decorator + * @package Calendar + * @access public + */ +class Calendar_Decorator_Wrapper extends Calendar_Decorator +{ + /** + * Constructs Calendar_Decorator_Wrapper + * @param object subclass of Calendar + * @access public + */ + function Calendar_Decorator_Wrapper(&$Calendar) + { + parent::Calendar_Decorator($Calendar); + } + + /** + * Wraps objects returned from fetch in the named Decorator class + * @param string name of Decorator class to wrap with + * @return object instance of named decorator + * @access public + */ + function & fetch($decorator) + { + $Calendar = parent::fetch(); + if ($Calendar) { + $ret =& new $decorator($Calendar); + } else { + $ret = false; + } + return $ret; + } + + /** + * Wraps the returned calendar objects from fetchAll in the named decorator + * @param string name of Decorator class to wrap with + * @return array + * @access public + */ + function fetchAll($decorator) + { + $children = parent::fetchAll(); + foreach ($children as $key => $Calendar) { + $children[$key] = & new $decorator($Calendar); + } + return $children; + } +} +?> \ No newline at end of file diff --git a/Calendar/Engine/Interface.php b/Calendar/Engine/Interface.php new file mode 100644 index 0000000..4c59e10 --- /dev/null +++ b/Calendar/Engine/Interface.php @@ -0,0 +1,293 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Interface.php,v 1.5 2004/08/16 12:29:18 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Interface.php,v 1.5 2004/08/16 12:29:18 hfuecks Exp $ + */ +/** + * The methods the classes implementing the Calendar_Engine must implement. + * Note this class is not used but simply to help development + * @package Calendar + * @access protected + */ +class Calendar_Engine_Interface +{ + /** + * Provides a mechansim to make sure parsing of timestamps + * into human dates is only performed once per timestamp. + * Typically called "internally" by methods like stampToYear. + * Return value can vary, depending on the specific implementation + * @param int timestamp (depending on implementation) + * @return mixed + * @access protected + */ + function stampCollection($stamp) + { + } + + /** + * Returns a numeric year given a timestamp + * @param int timestamp (depending on implementation) + * @return int year (e.g. 2003) + * @access protected + */ + function stampToYear($stamp) + { + } + + /** + * Returns a numeric month given a timestamp + * @param int timestamp (depending on implementation) + * @return int month (e.g. 9) + * @access protected + */ + function stampToMonth($stamp) + { + } + + /** + * Returns a numeric day given a timestamp + * @param int timestamp (depending on implementation) + * @return int day (e.g. 15) + * @access protected + */ + function stampToDay($stamp) + { + } + + /** + * Returns a numeric hour given a timestamp + * @param int timestamp (depending on implementation) + * @return int hour (e.g. 13) + * @access protected + */ + function stampToHour($stamp) + { + } + + /** + * Returns a numeric minute given a timestamp + * @param int timestamp (depending on implementation) + * @return int minute (e.g. 34) + * @access protected + */ + function stampToMinute($stamp) + { + } + + /** + * Returns a numeric second given a timestamp + * @param int timestamp (depending on implementation) + * @return int second (e.g. 51) + * @access protected + */ + function stampToSecond($stamp) + { + } + + /** + * Returns a timestamp. Can be worth "caching" generated + * timestamps in a static variable, identified by the + * params this method accepts, to timestamp will only + * be calculated once. + * @param int year (e.g. 2003) + * @param int month (e.g. 9) + * @param int day (e.g. 13) + * @param int hour (e.g. 13) + * @param int minute (e.g. 34) + * @param int second (e.g. 53) + * @return int (depends on implementation) + * @access protected + */ + function dateToStamp($y,$m,$d,$h,$i,$s) + { + } + + /** + * The upper limit on years that the Calendar Engine can work with + * @return int (e.g. 2037) + * @access protected + */ + function getMaxYears() + { + } + + /** + * The lower limit on years that the Calendar Engine can work with + * @return int (e.g 1902) + * @access protected + */ + function getMinYears() + { + } + + /** + * Returns the number of months in a year + * @param int (optional) year to get months for + * @return int (e.g. 12) + * @access protected + */ + function getMonthsInYear($y=null) + { + } + + /** + * Returns the number of days in a month, given year and month + * @param int year (e.g. 2003) + * @param int month (e.g. 9) + * @return int days in month + * @access protected + */ + function getDaysInMonth($y, $m) + { + } + + /** + * Returns numeric representation of the day of the week in a month, + * given year and month + * @param int year (e.g. 2003) + * @param int month (e.g. 9) + * @return int + * @access protected + */ + function getFirstDayInMonth ($y, $m) + { + } + + /** + * Returns the number of days in a week + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (e.g. 7) + * @access protected + */ + function getDaysInWeek($y=NULL, $m=NULL, $d=NULL) + { + } + + /** + * Returns the number of the week in the year (ISO-8601), given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int week number + * @access protected + */ + function getWeekNInYear($y, $m, $d) + { + } + + /** + * Returns the number of the week in the month, given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @param int first day of the week (default: 1 - monday) + * @return int week number + * @access protected + */ + function getWeekNInMonth($y, $m, $d, $firstDay=1) + { + } + + /** + * Returns the number of weeks in the month + * @param int year (2003) + * @param int month (9) + * @param int first day of the week (default: 1 - monday) + * @return int weeks number + * @access protected + */ + function getWeeksInMonth($y, $m) + { + } + + /** + * Returns the number of the day of the week (0=sunday, 1=monday...) + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int weekday number + * @access protected + */ + function getDayOfWeek($y, $m, $d) + { + } + + /** + * Returns the numeric values of the days of the week. + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return array list of numeric values of days in week, beginning 0 + * @access protected + */ + function getWeekDays($y=NULL, $m=NULL, $d=NULL) + { + } + + /** + * Returns the default first day of the week as an integer. Must be a + * member of the array returned from getWeekDays + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (e.g. 1 for Monday) + * @see getWeekDays + * @access protected + */ + function getFirstDayOfWeek($y=NULL, $m=NULL, $d=NULL) + { + } + + /** + * Returns the number of hours in a day
+ * @param int (optional) day to get hours for + * @return int (e.g. 24) + * @access protected + */ + function getHoursInDay($y=null,$m=null,$d=null) + { + } + + /** + * Returns the number of minutes in an hour + * @param int (optional) hour to get minutes for + * @return int + * @access protected + */ + function getMinutesInHour($y=null,$m=null,$d=null,$h=null) + { + } + + /** + * Returns the number of seconds in a minutes + * @param int (optional) minute to get seconds for + * @return int + * @access protected + */ + function getSecondsInMinute($y=null,$m=null,$d=null,$h=null,$i=null) + { + } +} +?> \ No newline at end of file diff --git a/Calendar/Engine/PearDate.php b/Calendar/Engine/PearDate.php new file mode 100644 index 0000000..22270c7 --- /dev/null +++ b/Calendar/Engine/PearDate.php @@ -0,0 +1,407 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: PearDate.php,v 1.8 2004/08/20 20:00:55 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: PearDate.php,v 1.8 2004/08/20 20:00:55 quipo Exp $ + */ +/** + * Load PEAR::Date class + */ +require_once 'Date.php'; + +/** + * Performs calendar calculations based on the PEAR::Date class + * Timestamps are in the ISO-8601 format (YYYY-MM-DD HH:MM:SS) + * @package Calendar + * @access protected + */ +class Calendar_Engine_PearDate /* implements Calendar_Engine_Interface */ +{ + /** + * Makes sure a given timestamp is only ever parsed once + * Uses a static variable to prevent date() being used twice + * for a date which is already known + * @param mixed Any timestamp format recognized by Pear::Date + * @return object Pear::Date object + * @access protected + */ + function stampCollection($stamp) + { + static $stamps = array(); + if (!isset($stamps[$stamp])) { + $stamps[$stamp] = new Date($stamp); + } + return $stamps[$stamp]; + } + + /** + * Returns a numeric year given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int year (e.g. 2003) + * @access protected + */ + function stampToYear($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->year; + } + + /** + * Returns a numeric month given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int month (e.g. 9) + * @access protected + */ + function stampToMonth($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->month; + } + + /** + * Returns a numeric day given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int day (e.g. 15) + * @access protected + */ + function stampToDay($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->day; + } + + /** + * Returns a numeric hour given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int hour (e.g. 13) + * @access protected + */ + function stampToHour($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->hour; + } + + /** + * Returns a numeric minute given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int minute (e.g. 34) + * @access protected + */ + function stampToMinute($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->minute; + } + + /** + * Returns a numeric second given a iso-8601 datetime + * @param string iso-8601 datetime (YYYY-MM-DD HH:MM:SS) + * @return int second (e.g. 51) + * @access protected + */ + function stampToSecond($stamp) + { + $date = Calendar_Engine_PearDate::stampCollection($stamp); + return (int)$date->second; + } + + /** + * Returns a iso-8601 datetime + * @param int year (2003) + * @param int month (9) + * @param int day (13) + * @param int hour (13) + * @param int minute (34) + * @param int second (53) + * @return string iso-8601 datetime + * @access protected + */ + function dateToStamp($y, $m, $d, $h=0, $i=0, $s=0) + { + $r = array(); + Calendar_Engine_PearDate::adjustDate($y, $m, $d, $h, $i, $s); + $key = $y.$m.$d.$h.$i.$s; + if (!isset($r[$key])) { + $r[$key] = sprintf("%04d-%02d-%02d %02d:%02d:%02d", + $y, $m, $d, $h, $i, $s); + } + return $r[$key]; + } + + /** + * Set the correct date values (useful for math operations on dates) + * @param int year (2003) + * @param int month (9) + * @param int day (13) + * @param int hour (13) + * @param int minute (34) + * @param int second (53) + * @access protected + */ + function adjustDate(&$y, &$m, &$d, &$h, &$i, &$s) + { + if ($s < 0) { + $m -= floor($s / 60); + $s = -$s % 60; + } + if ($s > 60) { + $m += floor($s / 60); + $s %= 60; + } + if ($i < 0) { + $h -= floor($i / 60); + $i = -$i % 60; + } + if ($i > 60) { + $h += floor($i / 60); + $i %= 60; + } + if ($h < 0) { + $d -= floor($h / 24); + $h = -$h % 24; + } + if ($h > 24) { + $d += floor($h / 24); + $h %= 24; + } + for(; $m < 1; $y--, $m+=12); + for(; $m > 12; $y++, $m-=12); + + while ($d < 1) { + if ($m > 1) { + $m--; + } else { + $m = 12; + $y--; + } + $d += Date_Calc::daysInMonth($m, $y); + } + for ($max_days = Date_Calc::daysInMonth($m, $y); $d > $max_days; ) { + $d -= $max_days; + if ($m < 12) { + $m++; + } else { + $m = 1; + $y++; + } + } + } + + /** + * The upper limit on years that the Calendar Engine can work with + * @return int 9999 + * @access protected + */ + function getMaxYears() + { + return 9999; + } + + /** + * The lower limit on years that the Calendar Engine can work with + * @return int 0 + * @access protected + */ + function getMinYears() + { + return 0; + } + + /** + * Returns the number of months in a year + * @return int (12) + * @access protected + */ + function getMonthsInYear($y=null) + { + return 12; + } + + /** + * Returns the number of days in a month, given year and month + * @param int year (2003) + * @param int month (9) + * @return int days in month + * @access protected + */ + function getDaysInMonth($y, $m) + { + return (int)Date_Calc::daysInMonth($m, $y); + } + + /** + * Returns numeric representation of the day of the week in a month, + * given year and month + * @param int year (2003) + * @param int month (9) + * @return int from 0 to 7 + * @access protected + */ + function getFirstDayInMonth($y, $m) + { + return (int)Date_Calc::dayOfWeek(1, $m, $y); + } + + /** + * Returns the number of days in a week + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (7) + * @access protected + */ + function getDaysInWeek($y=NULL, $m=NULL, $d=NULL) + { + return 7; + } + + /** + * Returns the number of the week in the year (ISO-8601), given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int week number + * @access protected + */ + function getWeekNInYear($y, $m, $d) + { + return Date_Calc::weekOfYear($d, $m, $y); //beware, Date_Calc doesn't follow ISO-8601 standard! + } + + /** + * Returns the number of the week in the month, given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @param int first day of the week (default: monday) + * @return int week number + * @access protected + */ + function getWeekNInMonth($y, $m, $d, $firstDay=1) + { + $weekEnd = ($firstDay == 0) ? $this->getDaysInWeek()-1 : $firstDay-1; + $end_of_week = (int)Date_Calc::nextDayOfWeek($weekEnd, 1, $m, $y, '%e', true); + $w = 1; + while ($d > $end_of_week) { + ++$w; + $end_of_week += $this->getDaysInWeek(); + } + return $w; + } + + /** + * Returns the number of weeks in the month + * @param int year (2003) + * @param int month (9) + * @param int first day of the week (default: monday) + * @return int weeks number + * @access protected + */ + function getWeeksInMonth($y, $m, $firstDay=1) + { + $FDOM = Date_Calc::firstOfMonthWeekday($m, $y); + if ($FDOM == 0) { + $FDOM = $this->getDaysInWeek(); + } + if ($FDOM > $firstDay) { + $daysInTheFirstWeek = $this->getDaysInWeek() - $FDOM + $firstDay; + $weeks = 1; + } else { + $daysInTheFirstWeek = $firstDay - $FDOM; + $weeks = 0; + } + $daysInTheFirstWeek %= $this->getDaysInWeek(); + return (int)(ceil(($this->getDaysInMonth($y, $m) - $daysInTheFirstWeek) / + $this->getDaysInWeek()) + $weeks); + } + + /** + * Returns the number of the day of the week (0=sunday, 1=monday...) + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int weekday number + * @access protected + */ + function getDayOfWeek($y, $m, $d) + { + return Date_Calc::dayOfWeek($d, $m, $y); + } + + /** + * Returns a list of integer days of the week beginning 0 + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return array (0, 1, 2, 3, 4, 5, 6) 1 = Monday + * @access protected + */ + function getWeekDays($y=NULL, $m=NULL, $d=NULL) + { + return array(0, 1, 2, 3, 4, 5, 6); + } + + /** + * Returns the default first day of the week + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (default 1 = Monday) + * @access protected + */ + function getFirstDayOfWeek($y=NULL, $m=NULL, $d=NULL) + { + return 1; + } + + /** + * Returns the number of hours in a day + * @return int (24) + * @access protected + */ + function getHoursInDay($y=null,$m=null,$d=null) + { + return 24; + } + + /** + * Returns the number of minutes in an hour + * @return int (60) + * @access protected + */ + function getMinutesInHour($y=null,$m=null,$d=null,$h=null) + { + return 60; + } + + /** + * Returns the number of seconds in a minutes + * @return int (60) + * @access protected + */ + function getSecondsInMinute($y=null,$m=null,$d=null,$h=null,$i=null) + { + return 60; + } +} +?> \ No newline at end of file diff --git a/Calendar/Engine/UnixTS.php b/Calendar/Engine/UnixTS.php new file mode 100644 index 0000000..5fb40e4 --- /dev/null +++ b/Calendar/Engine/UnixTS.php @@ -0,0 +1,365 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: UnixTS.php,v 1.9 2004/08/20 20:00:55 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: UnixTS.php,v 1.9 2004/08/20 20:00:55 quipo Exp $ + */ +/** + * Performs calendar calculations based on the PHP date() function and + * Unix timestamps (using PHP's mktime() function). + * @package Calendar + * @access protected + */ +class Calendar_Engine_UnixTS /* implements Calendar_Engine_Interface */ +{ + /** + * Makes sure a given timestamp is only ever parsed once + *
+     * array (
+     *  [0] => year (e.g 2003),
+     *  [1] => month (e.g 9),
+     *  [2] => day (e.g 6),
+     *  [3] => hour (e.g 14),
+     *  [4] => minute (e.g 34),
+     *  [5] => second (e.g 45),
+     *  [6] => num days in month (e.g. 31),
+     *  [7] => week in year (e.g. 50),
+     *  [8] => day in week (e.g. 0 for Sunday)
+     * )
+     * 
+ * Uses a static variable to prevent date() being used twice + * for a date which is already known + * @param int Unix timestamp + * @return array + * @access protected + */ + function stampCollection($stamp) + { + static $stamps = array(); + if ( !isset($stamps[$stamp]) ) { + $date = @date('Y n j H i s t W w',$stamp); + $stamps[$stamp] = sscanf($date, "%d %d %d %d %d %d %d %d %d"); + } + return $stamps[$stamp]; + } + + /** + * Returns a numeric year given a timestamp + * @param int Unix timestamp + * @return int year (e.g. 2003) + * @access protected + */ + function stampToYear($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[0]; + } + + /** + * Returns a numeric month given a timestamp + * @param int Unix timestamp + * @return int month (e.g. 9) + * @access protected + */ + function stampToMonth($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[1]; + } + + /** + * Returns a numeric day given a timestamp + * @param int Unix timestamp + * @return int day (e.g. 15) + * @access protected + */ + function stampToDay($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[2]; + } + + /** + * Returns a numeric hour given a timestamp + * @param int Unix timestamp + * @return int hour (e.g. 13) + * @access protected + */ + function stampToHour($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[3]; + } + + /** + * Returns a numeric minute given a timestamp + * @param int Unix timestamp + * @return int minute (e.g. 34) + * @access protected + */ + function stampToMinute($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[4]; + } + + /** + * Returns a numeric second given a timestamp + * @param int Unix timestamp + * @return int second (e.g. 51) + * @access protected + */ + function stampToSecond($stamp) + { + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return (int)$date[5]; + } + + /** + * Returns a timestamp + * @param int year (2003) + * @param int month (9) + * @param int day (13) + * @param int hour (13) + * @param int minute (34) + * @param int second (53) + * @return int Unix timestamp + * @access protected + */ + function dateToStamp($y, $m, $d, $h=0, $i=0, $s=0) + { + static $dates = array(); + if ( !isset($dates[$y][$m][$d][$h][$i][$s]) ) { + $dates[$y][$m][$d][$h][$i][$s] = @mktime($h, $i, $s, $m, $d, $y); + } + return $dates[$y][$m][$d][$h][$i][$s]; + } + + /** + * The upper limit on years that the Calendar Engine can work with + * @return int (2037) + * @access protected + */ + function getMaxYears() + { + return 2037; + } + + /** + * The lower limit on years that the Calendar Engine can work with + * @return int (1970 if it's Windows and 1902 for all other OSs) + * @access protected + */ + function getMinYears() + { + return $min = strpos(PHP_OS, 'WIN') === false ? 1902 : 1970; + } + + /** + * Returns the number of months in a year + * @return int (12) + * @access protected + */ + function getMonthsInYear($y=null) + { + return 12; + } + + /** + * Returns the number of days in a month, given year and month + * @param int year (2003) + * @param int month (9) + * @return int days in month + * @access protected + */ + function getDaysInMonth($y, $m) + { + $stamp = Calendar_Engine_UnixTS::dateToStamp($y,$m,1); + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return $date[6]; + } + + /** + * Returns numeric representation of the day of the week in a month, + * given year and month + * @param int year (2003) + * @param int month (9) + * @return int from 0 to 6 + * @access protected + */ + function getFirstDayInMonth($y, $m) + { + $stamp = Calendar_Engine_UnixTS::dateToStamp($y,$m,1); + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return $date[8]; + } + + /** + * Returns the number of days in a week + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (7) + * @access protected + */ + function getDaysInWeek($y=NULL, $m=NULL, $d=NULL) + { + return 7; + } + + /** + * Returns the number of the week in the year (ISO-8601), given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int week number + * @access protected + */ + function getWeekNInYear($y, $m, $d) + { + $stamp = Calendar_Engine_UnixTS::dateToStamp($y,$m,$d); + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return $date[7]; + } + + /** + * Returns the number of the week in the month, given a date + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @param int first day of the week (default: monday) + * @return int week number + * @access protected + */ + function getWeekNInMonth($y, $m, $d, $firstDay=1) + { + $weekEnd = ($firstDay == 0) ? $this->getDaysInWeek()-1 : $firstDay-1; + $end_of_week = 1; + while (@date('w', @mktime(0, 0, 0, $m, $end_of_week, $y)) != $weekEnd) { + ++$end_of_week; //find first weekend of the month + } + $w = 1; + while ($d > $end_of_week) { + ++$w; + $end_of_week += $this->getDaysInWeek(); + } + return $w; + } + + /** + * Returns the number of weeks in the month + * @param int year (2003) + * @param int month (9) + * @param int first day of the week (default: monday) + * @return int weeks number + * @access protected + */ + function getWeeksInMonth($y, $m, $firstDay=1) + { + $FDOM = $this->getFirstDayInMonth($y, $m); + if ($FDOM == 0) { + $FDOM = $this->getDaysInWeek(); + } + if ($FDOM > $firstDay) { + $daysInTheFirstWeek = $this->getDaysInWeek() - $FDOM + $firstDay; + $weeks = 1; + } else { + $daysInTheFirstWeek = $firstDay - $FDOM; + $weeks = 0; + } + $daysInTheFirstWeek %= $this->getDaysInWeek(); + return (int)(ceil(($this->getDaysInMonth($y, $m) - $daysInTheFirstWeek) / + $this->getDaysInWeek()) + $weeks); + } + + /** + * Returns the number of the day of the week (0=sunday, 1=monday...) + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int weekday number + * @access protected + */ + function getDayOfWeek($y, $m, $d) + { + $stamp = Calendar_Engine_UnixTS::dateToStamp($y,$m,$d); + $date = Calendar_Engine_UnixTS::stampCollection($stamp); + return $date[8]; + } + + /** + * Returns a list of integer days of the week beginning 0 + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return array (0,1,2,3,4,5,6) 1 = Monday + * @access protected + */ + function getWeekDays($y=NULL, $m=NULL, $d=NULL) + { + return array(0, 1, 2, 3, 4, 5, 6); + } + + /** + * Returns the default first day of the week + * @param int year (2003) + * @param int month (9) + * @param int day (4) + * @return int (default 1 = Monday) + * @access protected + */ + function getFirstDayOfWeek($y=NULL, $m=NULL, $d=NULL) + { + return 1; + } + + /** + * Returns the number of hours in a day + * @return int (24) + * @access protected + */ + function getHoursInDay($y=null,$m=null,$d=null) + { + return 24; + } + + /** + * Returns the number of minutes in an hour + * @return int (60) + * @access protected + */ + function getMinutesInHour($y=null,$m=null,$d=null,$h=null) + { + return 60; + } + + /** + * Returns the number of seconds in a minutes + * @return int (60) + * @access protected + */ + function getSecondsInMinute($y=null,$m=null,$d=null,$h=null,$i=null) + { + return 60; + } +} +?> \ No newline at end of file diff --git a/Calendar/Factory.php b/Calendar/Factory.php new file mode 100644 index 0000000..db6bd80 --- /dev/null +++ b/Calendar/Factory.php @@ -0,0 +1,145 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Factory.php,v 1.3 2005/10/22 10:08:47 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Factory.php,v 1.3 2005/10/22 10:08:47 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Contains a factory method to return a Singleton instance of a class + * implementing the Calendar_Engine_Interface.
+ * For Month objects, to control type of month returned, use CALENDAR_MONTH_STATE + * constact e.g.; + * + * require_once 'Calendar/Factory.php'; + * define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH_WEEKDAYS); // Use Calendar_Month_Weekdays + * // define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH_WEEKS); // Use Calendar_Month_Weeks + * // define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH); // Use Calendar_Month + * + * It defaults to building Calendar_Month objects.
+ * Use the constract CALENDAR_FIRST_DAY_OF_WEEK to control the first day of the week + * for Month or Week objects (e.g. 0 = Sunday, 6 = Saturday) + * @package Calendar + * @access protected + */ +class Calendar_Factory +{ + /** + * Creates a calendar object given the type and units + * @param string class of calendar object to create + * @param int year + * @param int month + * @param int day + * @param int hour + * @param int minute + * @param int second + * @return object subclass of Calendar + * @access public + * @static + */ + function create($type, $y = 2000, $m = 1, $d = 1, $h = 0, $i = 0, $s = 0) + { + $firstDay = defined('CALENDAR_FIRST_DAY_OF_WEEK') ? CALENDAR_FIRST_DAY_OF_WEEK : 1; + switch ($type) { + case 'Day': + require_once CALENDAR_ROOT.'Day.php'; + return new Calendar_Day($y,$m,$d); + case 'Month': + // Set default state for which month type to build + if (!defined('CALENDAR_MONTH_STATE')) { + define('CALENDAR_MONTH_STATE', CALENDAR_USE_MONTH); + } + switch (CALENDAR_MONTH_STATE) { + case CALENDAR_USE_MONTH_WEEKDAYS: + require_once CALENDAR_ROOT.'Month/Weekdays.php'; + $class = 'Calendar_Month_Weekdays'; + break; + case CALENDAR_USE_MONTH_WEEKS: + require_once CALENDAR_ROOT.'Month/Weeks.php'; + $class = 'Calendar_Month_Weeks'; + break; + case CALENDAR_USE_MONTH: + default: + require_once CALENDAR_ROOT.'Month.php'; + $class = 'Calendar_Month'; + break; + } + return new $class($y, $m, $firstDay); + case 'Week': + require_once CALENDAR_ROOT.'Week.php'; + return new Calendar_Week($y, $m, $d, $firstDay); + case 'Hour': + require_once CALENDAR_ROOT.'Hour.php'; + return new Calendar_Hour($y, $m, $d, $h); + case 'Minute': + require_once CALENDAR_ROOT.'Minute.php'; + return new Calendar_Minute($y, $m, $d, $h, $i); + case 'Second': + require_once CALENDAR_ROOT.'Second.php'; + return new Calendar_Second($y,$m,$d,$h,$i,$s); + case 'Year': + require_once CALENDAR_ROOT.'Year.php'; + return new Calendar_Year($y); + default: + require_once 'PEAR.php'; + PEAR::raiseError( + 'Calendar_Factory::create() unrecognised type: '.$type, null, PEAR_ERROR_TRIGGER, + E_USER_NOTICE, 'Calendar_Factory::create()'); + return false; + } + } + /** + * Creates an instance of a calendar object, given a type and timestamp + * @param string type of object to create + * @param mixed timestamp (depending on Calendar engine being used) + * @return object subclass of Calendar + * @access public + * @static + */ + function & createByTimestamp($type, $stamp) + { + $cE = & Calendar_Engine_Factory::getEngine(); + $y = $cE->stampToYear($stamp); + $m = $cE->stampToMonth($stamp); + $d = $cE->stampToDay($stamp); + $h = $cE->stampToHour($stamp); + $i = $cE->stampToMinute($stamp); + $s = $cE->stampToSecond($stamp); + $cal = Calendar_Factory::create($type, $y, $m, $d, $h, $i, $s); + return $cal; + } +} +?> \ No newline at end of file diff --git a/Calendar/Hour.php b/Calendar/Hour.php new file mode 100644 index 0000000..02bc778 --- /dev/null +++ b/Calendar/Hour.php @@ -0,0 +1,113 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Hour.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Hour.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents an Hour and builds Minutes + * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Hour.php'; + * $Hour = & new Calendar_Hour(2003, 10, 21, 15); // Oct 21st 2003, 3pm + * $Hour->build(); // Build Calendar_Minute objects + * while ($Minute = & $Hour->fetch()) { + * echo $Minute->thisMinute().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Hour extends Calendar +{ + /** + * Constructs Calendar_Hour + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int day e.g. 11 + * @param int hour e.g. 13 + * @access public + */ + function Calendar_Hour($y, $m, $d, $h) + { + Calendar::Calendar($y, $m, $d, $h); + } + + /** + * Builds the Minutes in the Hour + * @param array (optional) Calendar_Minute objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates=array()) + { + require_once CALENDAR_ROOT.'Minute.php'; + $mIH = $this->cE->getMinutesInHour($this->year, $this->month, $this->day, + $this->hour); + for ($i=0; $i < $mIH; $i++) { + $this->children[$i]= + new Calendar_Minute($this->year, $this->month, $this->day, + $this->hour, $i); + } + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear() + && $this->month == $sDate->thisMonth() + && $this->day == $sDate->thisDay() + && $this->hour == $sDate->thisHour()) + { + $key = (int)$sDate->thisMinute(); + if (isset($this->children[$key])) { + $sDate->setSelected(); + $this->children[$key] = $sDate; + } + } + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Minute.php b/Calendar/Minute.php new file mode 100644 index 0000000..a7e0f33 --- /dev/null +++ b/Calendar/Minute.php @@ -0,0 +1,114 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Minute.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Minute.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Minute and builds Seconds + * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Minute.php'; + * $Minute = & new Calendar_Minute(2003, 10, 21, 15, 31); // Oct 21st 2003, 3:31pm + * $Minute->build(); // Build Calendar_Second objects + * while ($Second = & $Minute->fetch()) { + * echo $Second->thisSecond().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Minute extends Calendar +{ + /** + * Constructs Minute + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int day e.g. 11 + * @param int hour e.g. 13 + * @param int minute e.g. 31 + * @access public + */ + function Calendar_Minute($y, $m, $d, $h, $i) + { + Calendar::Calendar($y, $m, $d, $h, $i); + } + + /** + * Builds the Calendar_Second objects + * @param array (optional) Calendar_Second objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates=array()) + { + require_once CALENDAR_ROOT.'Second.php'; + $sIM = $this->cE->getSecondsInMinute($this->year, $this->month, + $this->day, $this->hour, $this->minute); + for ($i=0; $i < $sIM; $i++) { + $this->children[$i] = new Calendar_Second($this->year, $this->month, + $this->day, $this->hour, $this->minute, $i); + } + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear() + && $this->month == $sDate->thisMonth() + && $this->day == $sDate->thisDay() + && $this->hour == $sDate->thisHour() + && $this->minute == $sDate->thisMinute()) + { + $key = (int)$sDate->thisSecond(); + if (isset($this->children[$key])) { + $sDate->setSelected(); + $this->children[$key] = $sDate; + } + } + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Month.php b/Calendar/Month.php new file mode 100644 index 0000000..60c6831 --- /dev/null +++ b/Calendar/Month.php @@ -0,0 +1,114 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Month.php,v 1.3 2005/10/22 10:10:26 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Month.php,v 1.3 2005/10/22 10:10:26 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Month and builds Days + * + * require_once 'Calendar/Month.php'; + * $Month = & new Calendar_Month(2003, 10); // Oct 2003 + * $Month->build(); // Build Calendar_Day objects + * while ($Day = & $Month->fetch()) { + * echo $Day->thisDay().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Month extends Calendar +{ + /** + * Constructs Calendar_Month + * @param int $y year e.g. 2003 + * @param int $m month e.g. 5 + * @param int $firstDay first day of the week [optional] + * @access public + */ + function Calendar_Month($y, $m, $firstDay=null) + { + Calendar::Calendar($y, $m); + $this->firstDay = $this->defineFirstDayOfWeek($firstDay); + } + + /** + * Builds Day objects for this Month. Creates as many Calendar_Day objects + * as there are days in the month + * @param array (optional) Calendar_Day objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates=array()) + { + require_once CALENDAR_ROOT.'Day.php'; + $daysInMonth = $this->cE->getDaysInMonth($this->year, $this->month); + for ($i=1; $i<=$daysInMonth; $i++) { + $this->children[$i] = new Calendar_Day($this->year, $this->month, $i); + } + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear() + && $this->month == $sDate->thisMonth() + ) { + $key = $sDate->thisDay(); + if (isset($this->children[$key])) { + $sDate->setSelected(); + $class = strtolower(get_class($sDate)); + if ($class == 'calendar_day' || $class == 'calendar_decorator') { + $sDate->setFirst($this->children[$key]->isFirst()); + $sDate->setLast($this->children[$key]->isLast()); + } + $this->children[$key] = $sDate; + } + } + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Month/Weekdays.php b/Calendar/Month/Weekdays.php new file mode 100644 index 0000000..a6c5ada --- /dev/null +++ b/Calendar/Month/Weekdays.php @@ -0,0 +1,189 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Weekdays.php,v 1.4 2005/10/22 10:28:49 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Weekdays.php,v 1.4 2005/10/22 10:28:49 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Load base month + */ +require_once CALENDAR_ROOT.'Month.php'; + +/** + * Represents a Month and builds Days in tabular form
+ * + * require_once 'Calendar/Month/Weekdays.php'; + * $Month = & new Calendar_Month_Weekdays(2003, 10); // Oct 2003 + * $Month->build(); // Build Calendar_Day objects + * while ($Day = & $Month->fetch()) { + * if ($Day->isFirst()) { + * echo ''; + * } + * if ($Day->isEmpty()) { + * echo ' '; + * } else { + * echo ''.$Day->thisDay().''; + * } + * if ($Day->isLast()) { + * echo ''; + * } + * } + * + * @package Calendar + * @access public + */ +class Calendar_Month_Weekdays extends Calendar_Month +{ + /** + * Instance of Calendar_Table_Helper + * @var Calendar_Table_Helper + * @access private + */ + var $tableHelper; + + /** + * First day of the week + * @access private + * @var string + */ + var $firstDay; + + /** + * Constructs Calendar_Month_Weekdays + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int (optional) first day of week (e.g. 0 for Sunday, 2 for Tuesday etc.) + * @access public + */ + function Calendar_Month_Weekdays($y, $m, $firstDay=null) + { + Calendar_Month::Calendar_Month($y, $m, $firstDay); + } + + /** + * Builds Day objects in tabular form, to allow display of calendar month + * with empty cells if the first day of the week does not fall on the first + * day of the month. + * @see Calendar_Day::isEmpty() + * @see Calendar_Day_Base::isFirst() + * @see Calendar_Day_Base::isLast() + * @param array (optional) Calendar_Day objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates=array()) + { + require_once CALENDAR_ROOT.'Table/Helper.php'; + $this->tableHelper = & new Calendar_Table_Helper($this, $this->firstDay); + Calendar_Month::build($sDates); + $this->buildEmptyDaysBefore(); + $this->shiftDays(); + $this->buildEmptyDaysAfter(); + $this->setWeekMarkers(); + return true; + } + + /** + * Prepends empty days before the real days in the month + * @return void + * @access private + */ + function buildEmptyDaysBefore() + { + $eBefore = $this->tableHelper->getEmptyDaysBefore(); + for ($i=0; $i < $eBefore; $i++) { + $stamp = $this->cE->dateToStamp($this->year, $this->month, -$i); + $Day = new Calendar_Day( + $this->cE->stampToYear($stamp), + $this->cE->stampToMonth($stamp), + $this->cE->stampToDay($stamp)); + $Day->setEmpty(); + $Day->adjust(); + array_unshift($this->children, $Day); + } + } + + /** + * Shifts the array of children forward, if necessary + * @return void + * @access private + */ + function shiftDays() + { + if (isset ($this->children[0])) { + array_unshift($this->children, null); + unset($this->children[0]); + } + } + + /** + * Appends empty days after the real days in the month + * @return void + * @access private + */ + function buildEmptyDaysAfter() + { + $eAfter = $this->tableHelper->getEmptyDaysAfter(); + $sDOM = $this->tableHelper->getNumTableDaysInMonth(); + for ($i = 1; $i <= $sDOM-$eAfter; $i++) { + $Day = new Calendar_Day($this->year, $this->month+1, $i); + $Day->setEmpty(); + $Day->adjust(); + array_push($this->children, $Day); + } + } + + /** + * Sets the "markers" for the beginning and of a of week, in the + * built Calendar_Day children + * @return void + * @access private + */ + function setWeekMarkers() + { + $dIW = $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay() + ); + $sDOM = $this->tableHelper->getNumTableDaysInMonth(); + for ($i=1; $i <= $sDOM; $i+= $dIW) { + $this->children[$i]->setFirst(); + $this->children[$i+($dIW-1)]->setLast(); + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Month/Weeks.php b/Calendar/Month/Weeks.php new file mode 100644 index 0000000..0a5007a --- /dev/null +++ b/Calendar/Month/Weeks.php @@ -0,0 +1,139 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Weeks.php,v 1.3 2005/10/22 10:28:49 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Weeks.php,v 1.3 2005/10/22 10:28:49 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Load base month + */ +require_once CALENDAR_ROOT.'Month.php'; + +/** + * Represents a Month and builds Weeks + * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Month'.DIRECTORY_SEPARATOR.'Weeks.php'; + * $Month = & new Calendar_Month_Weeks(2003, 10); // Oct 2003 + * $Month->build(); // Build Calendar_Day objects + * while ($Week = & $Month->fetch()) { + * echo $Week->thisWeek().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Month_Weeks extends Calendar_Month +{ + /** + * Instance of Calendar_Table_Helper + * @var Calendar_Table_Helper + * @access private + */ + var $tableHelper; + + /** + * First day of the week + * @access private + * @var string + */ + var $firstDay; + + /** + * Constructs Calendar_Month_Weeks + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int (optional) first day of week (e.g. 0 for Sunday, 2 for Tuesday etc.) + * @access public + */ + function Calendar_Month_Weeks($y, $m, $firstDay=null) + { + Calendar_Month::Calendar_Month($y, $m, $firstDay); + } + + /** + * Builds Calendar_Week objects for the Month. Note that Calendar_Week + * builds Calendar_Day object in tabular form (with Calendar_Day->empty) + * @param array (optional) Calendar_Week objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates=array()) + { + require_once CALENDAR_ROOT.'Table/Helper.php'; + $this->tableHelper = & new Calendar_Table_Helper($this, $this->firstDay); + require_once CALENDAR_ROOT.'Week.php'; + $numWeeks = $this->tableHelper->getNumWeeks(); + for ($i=1, $d=1; $i<=$numWeeks; $i++, + $d+=$this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay()) ) { + $this->children[$i] = new Calendar_Week( + $this->year, $this->month, $d, $this->tableHelper->getFirstDay()); + } + //used to set empty days + $this->children[1]->setFirst(true); + $this->children[$numWeeks]->setLast(true); + + // Handle selected weeks here + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear() + && $this->month == $sDate->thisMonth()) + { + $key = $sDate->thisWeek('n_in_month'); + if (isset($this->children[$key])) { + $this->children[$key]->setSelected(); + } + } + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Second.php b/Calendar/Second.php new file mode 100644 index 0000000..ccdd193 --- /dev/null +++ b/Calendar/Second.php @@ -0,0 +1,98 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Second.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Second.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Second
+ * Note: Seconds do not build other objects + * so related methods are overridden to return NULL + * @package Calendar + */ +class Calendar_Second extends Calendar +{ + /** + * Constructs Second + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int day e.g. 11 + * @param int hour e.g. 13 + * @param int minute e.g. 31 + * @param int second e.g. 45 + */ + function Calendar_Second($y, $m, $d, $h, $i, $s) + { + Calendar::Calendar($y, $m, $d, $h, $i, $s); + } + + /** + * Overwrite build + * @return NULL + */ + function build() + { + return null; + } + + /** + * Overwrite fetch + * @return NULL + */ + function fetch() + { + return null; + } + + /** + * Overwrite fetchAll + * @return NULL + */ + function fetchAll() + { + return null; + } + + /** + * Overwrite size + * @return NULL + */ + function size() + { + return null; + } +} +?> \ No newline at end of file diff --git a/Calendar/Table/Helper.php b/Calendar/Table/Helper.php new file mode 100644 index 0000000..0c6c9ab --- /dev/null +++ b/Calendar/Table/Helper.php @@ -0,0 +1,280 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Helper.php,v 1.5 2005/10/22 09:51:53 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Helper.php,v 1.5 2005/10/22 09:51:53 quipo Exp $ + */ + +/** + * Used by Calendar_Month_Weekdays, Calendar_Month_Weeks and Calendar_Week to + * help with building the calendar in tabular form + * @package Calendar + * @access protected + */ +class Calendar_Table_Helper +{ + /** + * Instance of the Calendar object being helped. + * @var object + * @access private + */ + var $calendar; + + /** + * Instance of the Calendar_Engine + * @var object + * @access private + */ + var $cE; + + /** + * First day of the week + * @access private + * @var string + */ + var $firstDay; + + /** + * The seven days of the week named + * @access private + * @var array + */ + var $weekDays; + + /** + * Days of the week ordered with $firstDay at the beginning + * @access private + * @var array + */ + var $daysOfWeek = array(); + + /** + * Days of the month built from days of the week + * @access private + * @var array + */ + var $daysOfMonth = array(); + + /** + * Number of weeks in month + * @var int + * @access private + */ + var $numWeeks = null; + + /** + * Number of emtpy days before real days begin in month + * @var int + * @access private + */ + var $emptyBefore = 0; + + /** + * Constructs Calendar_Table_Helper + * @param object Calendar_Month_Weekdays, Calendar_Month_Weeks, Calendar_Week + * @param int (optional) first day of the week e.g. 1 for Monday + * @access protected + */ + function Calendar_Table_Helper(& $calendar, $firstDay=null) + { + $this->calendar = & $calendar; + $this->cE = & $calendar->getEngine(); + if (is_null($firstDay)) { + $firstDay = $this->cE->getFirstDayOfWeek( + $this->calendar->thisYear(), + $this->calendar->thisMonth(), + $this->calendar->thisDay() + ); + } + $this->firstDay = $firstDay; + $this->setFirstDay(); + $this->setDaysOfMonth(); + } + + /** + * Constructs $this->daysOfWeek based on $this->firstDay + * @return void + * @access private + */ + function setFirstDay() + { + $weekDays = $this->cE->getWeekDays( + $this->calendar->thisYear(), + $this->calendar->thisMonth(), + $this->calendar->thisDay() + ); + $endDays = array(); + $tmpDays = array(); + $begin = false; + foreach ($weekDays as $day) { + if ($begin) { + $endDays[] = $day; + } else if ($day === $this->firstDay) { + $begin = true; + $endDays[] = $day; + } else { + $tmpDays[] = $day; + } + } + $this->daysOfWeek = array_merge($endDays, $tmpDays); + } + + /** + * Constructs $this->daysOfMonth + * @return void + * @access private + */ + function setDaysOfMonth() + { + $this->daysOfMonth = $this->daysOfWeek; + $daysInMonth = $this->cE->getDaysInMonth( + $this->calendar->thisYear(), $this->calendar->thisMonth()); + $firstDayInMonth = $this->cE->getFirstDayInMonth( + $this->calendar->thisYear(), $this->calendar->thisMonth()); + $this->emptyBefore=0; + foreach ($this->daysOfMonth as $dayOfWeek) { + if ($firstDayInMonth == $dayOfWeek) { + break; + } + $this->emptyBefore++; + } + $this->numWeeks = ceil( + ($daysInMonth + $this->emptyBefore) + / + $this->cE->getDaysInWeek( + $this->calendar->thisYear(), + $this->calendar->thisMonth(), + $this->calendar->thisDay() + ) + ); + for ($i=1; $i < $this->numWeeks; $i++) { + $this->daysOfMonth = + array_merge($this->daysOfMonth, $this->daysOfWeek); + } + } + + /** + * Returns the first day of the month + * @see Calendar_Engine_Interface::getFirstDayOfWeek() + * @return int + * @access protected + */ + function getFirstDay() + { + return $this->firstDay; + } + + /** + * Returns the order array of days in a week + * @return int + * @access protected + */ + function getDaysOfWeek() + { + return $this->daysOfWeek; + } + + /** + * Returns the number of tabular weeks in a month + * @return int + * @access protected + */ + function getNumWeeks() + { + return $this->numWeeks; + } + + /** + * Returns the number of real days + empty days + * @return int + * @access protected + */ + function getNumTableDaysInMonth() + { + return count($this->daysOfMonth); + } + + /** + * Returns the number of empty days before the real days begin + * @return int + * @access protected + */ + function getEmptyDaysBefore() + { + return $this->emptyBefore; + } + + /** + * Returns the index of the last real day in the month + * @todo Potential performance optimization with static + * @return int + * @access protected + */ + function getEmptyDaysAfter() + { + // Causes bug when displaying more than one month +// static $index; +// if (!isset($index)) { + $index = $this->getEmptyDaysBefore() + $this->cE->getDaysInMonth( + $this->calendar->thisYear(), $this->calendar->thisMonth()); +// } + return $index; + } + + /** + * Returns the index of the last real day in the month, relative to the + * beginning of the tabular week it is part of + * @return int + * @access protected + */ + function getEmptyDaysAfterOffset() + { + $eAfter = $this->getEmptyDaysAfter(); + return $eAfter - ( + $this->cE->getDaysInWeek( + $this->calendar->thisYear(), + $this->calendar->thisMonth(), + $this->calendar->thisDay() + ) * ($this->numWeeks-1) ); + } + + /** + * Returns the timestamp of the first day of the current week + */ + function getWeekStart($y, $m, $d, $firstDay=1) + { + $dow = $this->cE->getDayOfWeek($y, $m, $d); + if ($dow > $firstDay) { + $d -= ($dow - $firstDay); + } + if ($dow < $firstDay) { + $d -= ( + $this->cE->getDaysInWeek( + $this->calendar->thisYear(), + $this->calendar->thisMonth(), + $this->calendar->thisDay() + ) - $firstDay + $dow); + } + return $this->cE->dateToStamp($y, $m, $d); + } +} +?> \ No newline at end of file diff --git a/Calendar/Util/Textual.php b/Calendar/Util/Textual.php new file mode 100644 index 0000000..cb293a7 --- /dev/null +++ b/Calendar/Util/Textual.php @@ -0,0 +1,239 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Textual.php,v 1.2 2004/08/16 13:13:09 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Textual.php,v 1.2 2004/08/16 13:13:09 hfuecks Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar decorator base class + */ +require_once CALENDAR_ROOT.'Decorator.php'; + +/** + * Static utlities to help with fetching textual representations of months and + * days of the week. + * @package Calendar + * @access public + */ +class Calendar_Util_Textual +{ + + /** + * Returns an array of 12 month names (first index = 1) + * @param string (optional) format of returned months (one,two,short or long) + * @return array + * @access public + * @static + */ + function monthNames($format='long') + { + $formats = array('one'=>'%b', 'two'=>'%b', 'short'=>'%b', 'long'=>'%B'); + if (!array_key_exists($format,$formats)) { + $format = 'long'; + } + $months = array(); + for ($i=1; $i<=12; $i++) { + $stamp = mktime(0, 0, 0, $i, 1, 2003); + $month = strftime($formats[$format], $stamp); + switch($format) { + case 'one': + $month = substr($month, 0, 1); + break; + case 'two': + $month = substr($month, 0, 2); + break; + } + $months[$i] = $month; + } + return $months; + } + + /** + * Returns an array of 7 week day names (first index = 0) + * @param string (optional) format of returned days (one,two,short or long) + * @return array + * @access public + * @static + */ + function weekdayNames($format='long') + { + $formats = array('one'=>'%a', 'two'=>'%a', 'short'=>'%a', 'long'=>'%A'); + if (!array_key_exists($format,$formats)) { + $format = 'long'; + } + $days = array(); + for ($i=0; $i<=6; $i++) { + $stamp = mktime(0, 0, 0, 11, $i+2, 2003); + $day = strftime($formats[$format], $stamp); + switch($format) { + case 'one': + $day = substr($day, 0, 1); + break; + case 'two': + $day = substr($day, 0, 2); + break; + } + $days[$i] = $day; + } + return $days; + } + + /** + * Returns textual representation of the previous month of the decorated calendar object + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function prevMonthName($Calendar, $format='long') + { + $months = Calendar_Util_Textual::monthNames($format); + return $months[$Calendar->prevMonth()]; + } + + /** + * Returns textual representation of the month of the decorated calendar object + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function thisMonthName($Calendar, $format='long') + { + $months = Calendar_Util_Textual::monthNames($format); + return $months[$Calendar->thisMonth()]; + } + + /** + * Returns textual representation of the next month of the decorated calendar object + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function nextMonthName($Calendar, $format='long') + { + $months = Calendar_Util_Textual::monthNames($format); + return $months[$Calendar->nextMonth()]; + } + + /** + * Returns textual representation of the previous day of week of the decorated calendar object + * Note: Requires PEAR::Date + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function prevDayName($Calendar, $format='long') + { + $days = Calendar_Util_Textual::weekdayNames($format); + $stamp = $Calendar->prevDay('timestamp'); + $cE = $Calendar->getEngine(); + require_once 'Date/Calc.php'; + $day = Date_Calc::dayOfWeek($cE->stampToDay($stamp), + $cE->stampToMonth($stamp), $cE->stampToYear($stamp)); + return $days[$day]; + } + + /** + * Returns textual representation of the day of week of the decorated calendar object + * Note: Requires PEAR::Date + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function thisDayName($Calendar, $format='long') + { + $days = Calendar_Util_Textual::weekdayNames($format); + require_once 'Date/Calc.php'; + $day = Date_Calc::dayOfWeek($Calendar->thisDay(), $Calendar->thisMonth(), $Calendar->thisYear()); + return $days[$day]; + } + + /** + * Returns textual representation of the next day of week of the decorated calendar object + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return string + * @access public + * @static + */ + function nextDayName($Calendar, $format='long') + { + $days = Calendar_Util_Textual::weekdayNames($format); + $stamp = $Calendar->nextDay('timestamp'); + $cE = $Calendar->getEngine(); + require_once 'Date/Calc.php'; + $day = Date_Calc::dayOfWeek($cE->stampToDay($stamp), + $cE->stampToMonth($stamp), $cE->stampToYear($stamp)); + return $days[$day]; + } + + /** + * Returns the days of the week using the order defined in the decorated + * calendar object. Only useful for Calendar_Month_Weekdays, Calendar_Month_Weeks + * and Calendar_Week. Otherwise the returned array will begin on Sunday + * @param object subclass of Calendar e.g. Calendar_Month + * @param string (optional) format of returned months (one,two,short or long) + * @return array ordered array of week day names + * @access public + * @static + */ + function orderedWeekdays($Calendar, $format='long') + { + $days = Calendar_Util_Textual::weekdayNames($format); + + // Not so good - need methods to access this information perhaps... + if (isset($Calendar->tableHelper)) { + $ordereddays = $Calendar->tableHelper->daysOfWeek; + } else { + $ordereddays = array(0, 1, 2, 3, 4, 5, 6); + } + + $ordereddays = array_flip($ordereddays); + $i = 0; + $returndays = array(); + foreach ($ordereddays as $key => $value) { + $returndays[$i] = $days[$key]; + $i++; + } + return $returndays; + } +} +?> \ No newline at end of file diff --git a/Calendar/Util/Uri.php b/Calendar/Util/Uri.php new file mode 100644 index 0000000..05086dd --- /dev/null +++ b/Calendar/Util/Uri.php @@ -0,0 +1,169 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Uri.php,v 1.1 2004/08/16 09:03:55 hfuecks Exp $ +// +/** + * @package Calendar + * @version $Id: Uri.php,v 1.1 2004/08/16 09:03:55 hfuecks Exp $ + */ + +/** + * Utility to help building HTML links for navigating the calendar
+ * + * $Day = new Calendar_Day(2003, 10, 23); + * $Uri = & new Calendar_Util_Uri('year', 'month', 'day'); + * echo $Uri->prev($Day,'month'); // Displays year=2003&month=10 + * echo $Uri->prev($Day,'day'); // Displays year=2003&month=10&day=22 + * $Uri->seperator = '/'; + * $Uri->scalar = true; + * echo $Uri->prev($Day,'month'); // Displays 2003/10 + * echo $Uri->prev($Day,'day'); // Displays 2003/10/22 + * + * @package Calendar + * @access public + */ +class Calendar_Util_Uri +{ + /** + * Uri fragments for year, month, day etc. + * @var array + * @access private + */ + var $uris = array(); + + /** + * String to separate fragments with. + * Set to just & for HTML. + * For a scalar URL you might use / as the seperator + * @var string (default XHTML &) + * @access public + */ + var $separator = '&'; + + /** + * To output a "scalar" string - variable names omitted. + * Used for urls like index.php/2004/8/12 + * @var boolean (default false) + * @access public + */ + var $scalar = false; + + /** + * Constructs Calendar_Decorator_Uri + * The term "fragment" means name of a calendar GET variables in the URL + * @param string URI fragment for year + * @param string (optional) URI fragment for month + * @param string (optional) URI fragment for day + * @param string (optional) URI fragment for hour + * @param string (optional) URI fragment for minute + * @param string (optional) URI fragment for second + * @access public + */ + function Calendar_Util_Uri($y, $m=null, $d=null, $h=null, $i=null, $s=null) + { + $this->setFragments($y, $m, $d, $h, $i, $s); + } + + /** + * Sets the URI fragment names + * @param string URI fragment for year + * @param string (optional) URI fragment for month + * @param string (optional) URI fragment for day + * @param string (optional) URI fragment for hour + * @param string (optional) URI fragment for minute + * @param string (optional) URI fragment for second + * @return void + * @access public + */ + function setFragments($y, $m=null, $d=null, $h=null, $i=null, $s=null) { + if (!is_null($y)) $this->uris['Year'] = $y; + if (!is_null($m)) $this->uris['Month'] = $m; + if (!is_null($d)) $this->uris['Day'] = $d; + if (!is_null($h)) $this->uris['Hour'] = $h; + if (!is_null($i)) $this->uris['Minute'] = $i; + if (!is_null($s)) $this->uris['Second'] = $s; + } + + /** + * Gets the URI string for the previous calendar unit + * @param object subclassed from Calendar e.g. Calendar_Month + * @param string calendar unit ( must be year, month, week, day, hour, minute or second) + * @return string + * @access public + */ + function prev($Calendar, $unit) + { + $method = 'prev'.$unit; + $stamp = $Calendar->{$method}('timestamp'); + return $this->buildUriString($Calendar, $method, $stamp); + } + + /** + * Gets the URI string for the current calendar unit + * @param object subclassed from Calendar e.g. Calendar_Month + * @param string calendar unit ( must be year, month, week, day, hour, minute or second) + * @return string + * @access public + */ + function this($Calendar, $unit) + { + $method = 'this'.$unit; + $stamp = $Calendar->{$method}('timestamp'); + return $this->buildUriString($Calendar, $method, $stamp); + } + + /** + * Gets the URI string for the next calendar unit + * @param object subclassed from Calendar e.g. Calendar_Month + * @param string calendar unit ( must be year, month, week, day, hour, minute or second) + * @return string + * @access public + */ + function next($Calendar, $unit) + { + $method = 'next'.$unit; + $stamp = $Calendar->{$method}('timestamp'); + return $this->buildUriString($Calendar, $method, $stamp); + } + + /** + * Build the URI string + * @param string method substring + * @param int timestamp + * @return string build uri string + * @access private + */ + function buildUriString($Calendar, $method, $stamp) + { + $uriString = ''; + $cE = & $Calendar->getEngine(); + $separator = ''; + foreach ($this->uris as $unit => $uri) { + $call = 'stampTo'.$unit; + $uriString .= $separator; + if (!$this->scalar) $uriString .= $uri.'='; + $uriString .= $cE->{$call}($stamp); + $separator = $this->separator; + } + return $uriString; + } +} +?> \ No newline at end of file diff --git a/Calendar/Validator.php b/Calendar/Validator.php new file mode 100644 index 0000000..3cdd73d --- /dev/null +++ b/Calendar/Validator.php @@ -0,0 +1,335 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Validator.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Validator.php,v 1.1 2004/05/24 22:25:42 quipo Exp $ + */ + +/** + * Validation Error Messages + */ +if (!defined('CALENDAR_VALUE_TOOSMALL')) { + define('CALENDAR_VALUE_TOOSMALL', 'Too small: min = '); +} +if (!defined('CALENDAR_VALUE_TOOLARGE')) { + define('CALENDAR_VALUE_TOOLARGE', 'Too large: max = '); +} + +/** + * Used to validate any given Calendar date object. Instances of this class + * can be obtained from any data object using the getValidator method + * @see Calendar::getValidator() + * @package Calendar + * @access public + */ +class Calendar_Validator +{ + /** + * Instance of the Calendar date object to validate + * @var object + * @access private + */ + var $calendar; + + /** + * Instance of the Calendar_Engine + * @var object + * @access private + */ + var $cE; + + /** + * Array of errors for validation failures + * @var array + * @access private + */ + var $errors = array(); + + /** + * Constructs Calendar_Validator + * @param object subclass of Calendar + * @access public + */ + function Calendar_Validator(& $calendar) + { + $this->calendar = & $calendar; + $this->cE = & $calendar->getEngine(); + } + + /** + * Calls all the other isValidXXX() methods in the validator + * @return boolean + * @access public + */ + function isValid() + { + $checks = array('isValidYear', 'isValidMonth', 'isValidDay', + 'isValidHour', 'isValidMinute', 'isValidSecond'); + $valid = true; + foreach ($checks as $check) { + if (!$this->{$check}()) { + $valid = false; + } + } + return $valid; + } + + /** + * Check whether this is a valid year + * @return boolean + * @access public + */ + function isValidYear() + { + $y = $this->calendar->thisYear(); + $min = $this->cE->getMinYears(); + if ($min > $y) { + $this->errors[] = new Calendar_Validation_Error( + 'Year', $y, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = $this->cE->getMaxYears(); + if ($y > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Year', $y, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Check whether this is a valid month + * @return boolean + * @access public + */ + function isValidMonth() + { + $m = $this->calendar->thisMonth(); + $min = 1; + if ($min > $m) { + $this->errors[] = new Calendar_Validation_Error( + 'Month', $m, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = $this->cE->getMonthsInYear($this->calendar->thisYear()); + if ($m > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Month', $m, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Check whether this is a valid day + * @return boolean + * @access public + */ + function isValidDay() + { + $d = $this->calendar->thisDay(); + $min = 1; + if ($min > $d) { + $this->errors[] = new Calendar_Validation_Error( + 'Day', $d, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = $this->cE->getDaysInMonth( + $this->calendar->thisYear(), $this->calendar->thisMonth()); + if ($d > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Day', $d, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Check whether this is a valid hour + * @return boolean + * @access public + */ + function isValidHour() + { + $h = $this->calendar->thisHour(); + $min = 0; + if ($min > $h) { + $this->errors[] = new Calendar_Validation_Error( + 'Hour', $h, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = ($this->cE->getHoursInDay($this->calendar->thisDay())-1); + if ($h > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Hour', $h, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Check whether this is a valid minute + * @return boolean + * @access public + */ + function isValidMinute() + { + $i = $this->calendar->thisMinute(); + $min = 0; + if ($min > $i) { + $this->errors[] = new Calendar_Validation_Error( + 'Minute', $i, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = ($this->cE->getMinutesInHour($this->calendar->thisHour())-1); + if ($i > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Minute', $i, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Check whether this is a valid second + * @return boolean + * @access public + */ + function isValidSecond() + { + $s = $this->calendar->thisSecond(); + $min = 0; + if ($min > $s) { + $this->errors[] = new Calendar_Validation_Error( + 'Second', $s, CALENDAR_VALUE_TOOSMALL.$min); + return false; + } + $max = ($this->cE->getSecondsInMinute($this->calendar->thisMinute())-1); + if ($s > $max) { + $this->errors[] = new Calendar_Validation_Error( + 'Second', $s, CALENDAR_VALUE_TOOLARGE.$max); + return false; + } + return true; + } + + /** + * Iterates over any validation errors + * @return mixed either Calendar_Validation_Error or false + * @access public + */ + function fetch() + { + $error = each ($this->errors); + if ($error) { + return $error['value']; + } else { + reset($this->errors); + return false; + } + } +} + +/** + * For Validation Error messages + * @see Calendar::fetch() + * @package Calendar + * @access public + */ +class Calendar_Validation_Error +{ + /** + * Date unit (e.g. month,hour,second) which failed test + * @var string + * @access private + */ + var $unit; + + /** + * Value of unit which failed test + * @var int + * @access private + */ + var $value; + + /** + * Validation error message + * @var string + * @access private + */ + var $message; + + /** + * Constructs Calendar_Validation_Error + * @param string Date unit (e.g. month,hour,second) + * @param int Value of unit which failed test + * @param string Validation error message + * @access protected + */ + function Calendar_Validation_Error($unit,$value,$message) + { + $this->unit = $unit; + $this->value = $value; + $this->message = $message; + } + + /** + * Returns the Date unit + * @return string + * @access public + */ + function getUnit() + { + return $this->unit; + } + + /** + * Returns the value of the unit + * @return int + * @access public + */ + function getValue() + { + return $this->value; + } + + /** + * Returns the validation error message + * @return string + * @access public + */ + function getMessage() + { + return $this->message; + } + + /** + * Returns a string containing the unit, value and error message + * @return string + * @access public + */ + function toString () + { + return $this->unit.' = '.$this->value.' ['.$this->message.']'; + } +} +?> \ No newline at end of file diff --git a/Calendar/Week.php b/Calendar/Week.php new file mode 100644 index 0000000..33f6027 --- /dev/null +++ b/Calendar/Week.php @@ -0,0 +1,394 @@ + | +// | Lorenzo Alberton | +// +----------------------------------------------------------------------+ +// +// $Id: Week.php,v 1.7 2005/10/22 10:26:49 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Week.php,v 1.7 2005/10/22 10:26:49 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Week and builds Days in tabular format
+ * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Week.php'; + * $Week = & new Calendar_Week(2003, 10, 1); Oct 2003, 1st tabular week + * echo ''; + * while ($Day = & $Week->fetch()) { + * if ($Day->isEmpty()) { + * echo ' '; + * } else { + * echo ''.$Day->thisDay().''; + * } + * } + * echo ''; + * + * @package Calendar + * @access public + */ +class Calendar_Week extends Calendar +{ + /** + * Instance of Calendar_Table_Helper + * @var Calendar_Table_Helper + * @access private + */ + var $tableHelper; + + /** + * Stores the timestamp of the first day of this week + * @access private + * @var object + */ + var $thisWeek; + + /** + * Stores the timestamp of first day of previous week + * @access private + * @var object + */ + var $prevWeek; + + /** + * Stores the timestamp of first day of next week + * @access private + * @var object + */ + var $nextWeek; + + /** + * Used by build() to set empty days + * @access private + * @var boolean + */ + var $firstWeek = false; + + /** + * Used by build() to set empty days + * @access private + * @var boolean + */ + var $lastWeek = false; + + /** + * First day of the week (0=sunday, 1=monday...) + * @access private + * @var boolean + */ + var $firstDay = 1; + + /** + * Constructs Week + * @param int year e.g. 2003 + * @param int month e.g. 5 + * @param int a day of the desired week + * @param int (optional) first day of week (e.g. 0 for Sunday, 2 for Tuesday etc.) + * @access public + */ + function Calendar_Week($y, $m, $d, $firstDay=null) + { + require_once CALENDAR_ROOT.'Table/Helper.php'; + Calendar::Calendar($y, $m, $d); + $this->firstDay = $this->defineFirstDayOfWeek($firstDay); + $this->tableHelper = & new Calendar_Table_Helper($this, $this->firstDay); + $this->thisWeek = $this->tableHelper->getWeekStart($y, $m, $d, $this->firstDay); + $this->prevWeek = $this->tableHelper->getWeekStart($y, $m, $d - $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay()), $this->firstDay); + $this->nextWeek = $this->tableHelper->getWeekStart($y, $m, $d + $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay()), $this->firstDay); + } + + /** + * Defines the calendar by a timestamp (Unix or ISO-8601), replacing values + * passed to the constructor + * @param int|string Unix or ISO-8601 timestamp + * @return void + * @access public + */ + function setTimestamp($ts) + { + parent::setTimestamp($ts); + $this->thisWeek = $this->tableHelper->getWeekStart( + $this->year, $this->month, $this->day, $this->firstDay + ); + $this->prevWeek = $this->tableHelper->getWeekStart( + $this->year, $this->month, $this->day - $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay()), $this->firstDay + ); + $this->nextWeek = $this->tableHelper->getWeekStart( + $this->year, $this->month, $this->day + $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay()), $this->firstDay + ); + } + + /** + * Builds Calendar_Day objects for this Week + * @param array (optional) Calendar_Day objects representing selected dates + * @return boolean + * @access public + */ + function build($sDates = array()) + { + require_once CALENDAR_ROOT.'Day.php'; + $year = $this->cE->stampToYear($this->thisWeek); + $month = $this->cE->stampToMonth($this->thisWeek); + $day = $this->cE->stampToDay($this->thisWeek); + $end = $this->cE->getDaysInWeek( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay() + ); + + for ($i=1; $i <= $end; $i++) { + $stamp = $this->cE->dateToStamp($year, $month, $day++); + $this->children[$i] = new Calendar_Day( + $this->cE->stampToYear($stamp), + $this->cE->stampToMonth($stamp), + $this->cE->stampToDay($stamp)); + } + + //set empty days (@see Calendar_Month_Weeks::build()) + if ($this->firstWeek) { + $eBefore = $this->tableHelper->getEmptyDaysBefore(); + for ($i=1; $i <= $eBefore; $i++) { + $this->children[$i]->setEmpty(); + } + } + if ($this->lastWeek) { + $eAfter = $this->tableHelper->getEmptyDaysAfterOffset(); + for ($i = $eAfter+1; $i <= $end; $i++) { + $this->children[$i]->setEmpty(); + } + } + + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * @param boolean + * @return void + * @access private + */ + function setFirst($state=true) + { + $this->firstWeek = $state; + } + + /** + * @param boolean + * @return void + * @access private + */ + function setLast($state=true) + { + $this->lastWeek = $state; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) + { + foreach ($sDates as $sDate) { + foreach ($this->children as $key => $child) { + if ($child->thisDay() == $sDate->thisDay() && + $child->thisMonth() == $sDate->thisMonth() && + $child->thisYear() == $sDate->thisYear() + ) { + $this->children[$key] = $sDate; + $this->children[$key]->setSelected(); + } + } + } + reset($this->children); + } + + /** + * Gets the value of the previous week, according to the requested format + * + * @param string $format ['timestamp' | 'n_in_month' | 'n_in_year' | 'array'] + * @return mixed + * @access public + */ + function prevWeek($format = 'n_in_month') + { + switch (strtolower($format)) { + case 'int': + case 'n_in_month': + return ($this->firstWeek) ? null : $this->thisWeek('n_in_month') -1; + break; + case 'n_in_year': + return $this->cE->getWeekNInYear( + $this->cE->stampToYear($this->prevWeek), + $this->cE->stampToMonth($this->prevWeek), + $this->cE->stampToDay($this->prevWeek)); + break; + case 'array': + return $this->toArray($this->prevWeek); + break; + case 'object': + require_once CALENDAR_ROOT.'Factory.php'; + return Calendar_Factory::createByTimestamp('Week', $this->prevWeek); + break; + case 'timestamp': + default: + return $this->prevWeek; + break; + } + } + + /** + * Gets the value of the current week, according to the requested format + * + * @param string $format ['timestamp' | 'n_in_month' | 'n_in_year' | 'array'] + * @return mixed + * @access public + */ + function thisWeek($format = 'n_in_month') + { + switch (strtolower($format)) { + case 'int': + case 'n_in_month': + if ($this->firstWeek) { + return 1; + } + if ($this->lastWeek) { + return $this->cE->getWeeksInMonth( + $this->thisYear(), + $this->thisMonth(), + $this->firstDay); + } + return $this->cE->getWeekNInMonth( + $this->thisYear(), + $this->thisMonth(), + $this->thisDay(), + $this->firstDay); + break; + case 'n_in_year': + return $this->cE->getWeekNInYear( + $this->cE->stampToYear($this->thisWeek), + $this->cE->stampToMonth($this->thisWeek), + $this->cE->stampToDay($this->thisWeek)); + break; + case 'array': + return $this->toArray($this->thisWeek); + break; + case 'object': + require_once CALENDAR_ROOT.'Factory.php'; + return Calendar_Factory::createByTimestamp('Week', $this->thisWeek); + break; + case 'timestamp': + default: + return $this->thisWeek; + break; + } + } + + /** + * Gets the value of the following week, according to the requested format + * + * @param string $format ['timestamp' | 'n_in_month' | 'n_in_year' | 'array'] + * @return mixed + * @access public + */ + function nextWeek($format = 'n_in_month') + { + switch (strtolower($format)) { + case 'int': + case 'n_in_month': + return ($this->lastWeek) ? null : $this->thisWeek('n_in_month') +1; + break; + case 'n_in_year': + return $this->cE->getWeekNInYear( + $this->cE->stampToYear($this->nextWeek), + $this->cE->stampToMonth($this->nextWeek), + $this->cE->stampToDay($this->nextWeek)); + break; + case 'array': + return $this->toArray($this->nextWeek); + break; + case 'object': + require_once CALENDAR_ROOT.'Factory.php'; + return Calendar_Factory::createByTimestamp('Week', $this->nextWeek); + break; + case 'timestamp': + default: + return $this->nextWeek; + break; + } + } + + /** + * Returns the instance of Calendar_Table_Helper. + * Called from Calendar_Validator::isValidWeek + * @return Calendar_Table_Helper + * @access protected + */ + function & getHelper() + { + return $this->tableHelper; + } + + /** + * Makes sure theres a value for $this->day + * @return void + * @access private + */ + function findFirstDay() + { + if (!count($this->children) > 0) { + $this->build(); + foreach ($this->children as $Day) { + if (!$Day->isEmpty()) { + $this->day = $Day->thisDay(); + break; + } + } + } + } +} +?> \ No newline at end of file diff --git a/Calendar/Year.php b/Calendar/Year.php new file mode 100644 index 0000000..097db98 --- /dev/null +++ b/Calendar/Year.php @@ -0,0 +1,113 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Year.php,v 1.4 2005/10/22 10:25:39 quipo Exp $ +// +/** + * @package Calendar + * @version $Id: Year.php,v 1.4 2005/10/22 10:25:39 quipo Exp $ + */ + +/** + * Allows Calendar include path to be redefined + * @ignore + */ +if (!defined('CALENDAR_ROOT')) { + define('CALENDAR_ROOT', 'Calendar'.DIRECTORY_SEPARATOR); +} + +/** + * Load Calendar base class + */ +require_once CALENDAR_ROOT.'Calendar.php'; + +/** + * Represents a Year and builds Months
+ * + * require_once 'Calendar'.DIRECTORY_SEPARATOR.'Year.php'; + * $Year = & new Calendar_Year(2003, 10, 21); // 21st Oct 2003 + * $Year->build(); // Build Calendar_Month objects + * while ($Month = & $Year->fetch()) { + * echo $Month->thisMonth().'
'; + * } + *
+ * @package Calendar + * @access public + */ +class Calendar_Year extends Calendar +{ + /** + * Constructs Calendar_Year + * @param int year e.g. 2003 + * @access public + */ + function Calendar_Year($y) + { + Calendar::Calendar($y); + } + + /** + * Builds the Months of the Year.
+ * Note: by defining the constant CALENDAR_MONTH_STATE you can + * control what class of Calendar_Month is built e.g.; + * + * require_once 'Calendar/Calendar_Year.php'; + * define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH_WEEKDAYS); // Use Calendar_Month_Weekdays + * // define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH_WEEKS); // Use Calendar_Month_Weeks + * // define ('CALENDAR_MONTH_STATE',CALENDAR_USE_MONTH); // Use Calendar_Month + * + * It defaults to building Calendar_Month objects. + * @param array (optional) array of Calendar_Month objects representing selected dates + * @param int (optional) first day of week (e.g. 0 for Sunday, 2 for Tuesday etc.) + * @return boolean + * @access public + */ + function build($sDates = array(), $firstDay = null) + { + require_once CALENDAR_ROOT.'Factory.php'; + $this->firstDay = $this->defineFirstDayOfWeek($firstDay); + $monthsInYear = $this->cE->getMonthsInYear($this->thisYear()); + for ($i=1; $i <= $monthsInYear; $i++) { + $this->children[$i] = Calendar_Factory::create('Month', $this->year, $i); + } + if (count($sDates) > 0) { + $this->setSelection($sDates); + } + return true; + } + + /** + * Called from build() + * @param array + * @return void + * @access private + */ + function setSelection($sDates) { + foreach ($sDates as $sDate) { + if ($this->year == $sDate->thisYear()) { + $key = $sDate->thisMonth(); + if (isset($this->children[$key])) { + $sDate->setSelected(); + $this->children[$key] = $sDate; + } + } + } + } +} +?> \ No newline at end of file diff --git a/Console/Getopt.php b/Console/Getopt.php new file mode 100644 index 0000000..bb9d69c --- /dev/null +++ b/Console/Getopt.php @@ -0,0 +1,290 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Getopt.php,v 1.4 2007/06/12 14:58:56 cellog Exp $ + +require_once 'PEAR.php'; + +/** + * Command-line options parsing class. + * + * @author Andrei Zmievski + * + */ +class Console_Getopt { + /** + * Parses the command-line options. + * + * The first parameter to this function should be the list of command-line + * arguments without the leading reference to the running program. + * + * The second parameter is a string of allowed short options. Each of the + * option letters can be followed by a colon ':' to specify that the option + * requires an argument, or a double colon '::' to specify that the option + * takes an optional argument. + * + * The third argument is an optional array of allowed long options. The + * leading '--' should not be included in the option name. Options that + * require an argument should be followed by '=', and options that take an + * option argument should be followed by '=='. + * + * The return value is an array of two elements: the list of parsed + * options and the list of non-option command-line arguments. Each entry in + * the list of parsed options is a pair of elements - the first one + * specifies the option, and the second one specifies the option argument, + * if there was one. + * + * Long and short options can be mixed. + * + * Most of the semantics of this function are based on GNU getopt_long(). + * + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * + * @return array two-element array containing the list of parsed options and + * the non-option arguments + * + * @access public + * + */ + function getopt2($args, $short_options, $long_options = null) + { + return Console_Getopt::doGetopt(2, $args, $short_options, $long_options); + } + + /** + * This function expects $args to start with the script name (POSIX-style). + * Preserved for backwards compatibility. + * @see getopt2() + */ + function getopt($args, $short_options, $long_options = null) + { + return Console_Getopt::doGetopt(1, $args, $short_options, $long_options); + } + + /** + * The actual implementation of the argument parsing code. + */ + function doGetopt($version, $args, $short_options, $long_options = null) + { + // in case you pass directly readPHPArgv() as the first arg + if (PEAR::isError($args)) { + return $args; + } + if (empty($args)) { + return array(array(), array()); + } + $opts = array(); + $non_opts = array(); + + settype($args, 'array'); + + if ($long_options) { + sort($long_options); + } + + /* + * Preserve backwards compatibility with callers that relied on + * erroneous POSIX fix. + */ + if ($version < 2) { + if (isset($args[0]{0}) && $args[0]{0} != '-') { + array_shift($args); + } + } + + reset($args); + while (list($i, $arg) = each($args)) { + + /* The special element '--' means explicit end of + options. Treat the rest of the arguments as non-options + and end the loop. */ + if ($arg == '--') { + $non_opts = array_merge($non_opts, array_slice($args, $i + 1)); + break; + } + + if ($arg{0} != '-' || (strlen($arg) > 1 && $arg{1} == '-' && !$long_options)) { + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } elseif (strlen($arg) > 1 && $arg{1} == '-') { + $error = Console_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args); + if (PEAR::isError($error)) + return $error; + } elseif ($arg == '-') { + // - is stdin + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } else { + $error = Console_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args); + if (PEAR::isError($error)) + return $error; + } + } + + return array($opts, $non_opts); + } + + /** + * @access private + * + */ + function _parseShortOption($arg, $short_options, &$opts, &$args) + { + for ($i = 0; $i < strlen($arg); $i++) { + $opt = $arg{$i}; + $opt_arg = null; + + /* Try to find the short option in the specifier string. */ + if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') + { + return PEAR::raiseError("Console_Getopt: unrecognized option -- $opt"); + } + + if (strlen($spec) > 1 && $spec{1} == ':') { + if (strlen($spec) > 2 && $spec{2} == ':') { + if ($i + 1 < strlen($arg)) { + /* Option takes an optional argument. Use the remainder of + the arg string if there is anything left. */ + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } + } else { + /* Option requires an argument. Use the remainder of the arg + string if there is anything left. */ + if ($i + 1 < strlen($arg)) { + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } else if (list(, $opt_arg) = each($args)) { + /* Else use the next argument. */; + if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { + return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt"); + } + } else { + return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt"); + } + } + } + + $opts[] = array($opt, $opt_arg); + } + } + + /** + * @access private + * + */ + function _isShortOpt($arg) + { + return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]); + } + + /** + * @access private + * + */ + function _isLongOpt($arg) + { + return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' && + preg_match('/[a-zA-Z]+$/', substr($arg, 2)); + } + + /** + * @access private + * + */ + function _parseLongOption($arg, $long_options, &$opts, &$args) + { + @list($opt, $opt_arg) = explode('=', $arg, 2); + $opt_len = strlen($opt); + + for ($i = 0; $i < count($long_options); $i++) { + $long_opt = $long_options[$i]; + $opt_start = substr($long_opt, 0, $opt_len); + $long_opt_name = str_replace('=', '', $long_opt); + + /* Option doesn't match. Go on to the next one. */ + if ($long_opt_name != $opt) { + continue; + } + + $opt_rest = substr($long_opt, $opt_len); + + /* Check that the options uniquely matches one of the allowed + options. */ + if ($i + 1 < count($long_options)) { + $next_option_rest = substr($long_options[$i + 1], $opt_len); + } else { + $next_option_rest = ''; + } + if ($opt_rest != '' && $opt{0} != '=' && + $i + 1 < count($long_options) && + $opt == substr($long_options[$i+1], 0, $opt_len) && + $next_option_rest != '' && + $next_option_rest{0} != '=') { + return PEAR::raiseError("Console_Getopt: option --$opt is ambiguous"); + } + + if (substr($long_opt, -1) == '=') { + if (substr($long_opt, -2) != '==') { + /* Long option requires an argument. + Take the next argument if one wasn't specified. */; + if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) { + return PEAR::raiseError("Console_Getopt: option --$opt requires an argument"); + } + if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { + return PEAR::raiseError("Console_Getopt: option requires an argument --$opt"); + } + } + } else if ($opt_arg) { + return PEAR::raiseError("Console_Getopt: option --$opt doesn't allow an argument"); + } + + $opts[] = array('--' . $opt, $opt_arg); + return; + } + + return PEAR::raiseError("Console_Getopt: unrecognized option --$opt"); + } + + /** + * Safely read the $argv PHP array across different PHP configurations. + * Will take care on register_globals and register_argc_argv ini directives + * + * @access public + * @return mixed the $argv PHP array or PEAR error if not registered + */ + function readPHPArgv() + { + global $argv; + if (!is_array($argv)) { + if (!@is_array($_SERVER['argv'])) { + if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { + return PEAR::raiseError("Console_Getopt: Could not read cmd args (register_argc_argv=Off?)"); + } + return $GLOBALS['HTTP_SERVER_VARS']['argv']; + } + return $_SERVER['argv']; + } + return $argv; + } + +} + +?> diff --git a/Crypt/HMAC.php b/Crypt/HMAC.php new file mode 100644 index 0000000..70235be --- /dev/null +++ b/Crypt/HMAC.php @@ -0,0 +1,163 @@ + + * @author Matthew Fonda + * @copyright 1997-2005 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: HMAC.php,v 1.3 2005/02/20 19:18:29 mfonda Exp $ + * @link http://pear.php.net/package/Crypt_HMAC + */ + + + + +/** +* Calculates RFC 2104 compliant HMACs +* +* @access public +* @category Encryption +* @package Crypt_HMAC +* @author Derick Rethans +* @author Matthew Fonda +* @copyright 1997-2005 The PHP Group +* @license http://www.php.net/license/3_0.txt PHP License 3.0 +* @link http://pear.php.net/package/Crypt_HMAC +*/ +class Crypt_HMAC +{ + + /** + * Hash function to use + * @var string + * @access private + */ + var $_func; + + /** + * Inner padded key + * @var string + * @access private + */ + var $_ipad; + + /** + * Outer padded key + * @var string + * @access private + */ + var $_opad; + + /** + * Pack format + * @var string + * @access private + */ + var $_pack; + + + /** + * Constructor + * Pass method as first parameter + * + * @param string $key Key to use for hash + * @param string $func Hash function used for the calculation + * @return void + * @access public + */ + function Crypt_HMAC($key, $func = 'md5') + { + $this->setFunction($func); + $this->setKey($key); + } + + + /** + * Sets hash function + * + * @param string $func Hash function to use + * @return void + * @access public + */ + function setFunction($func) + { + if (!$this->_pack = $this->_getPackFormat($func)) { + die('Unsupported hash function'); + } + $this->_func = $func; + } + + + /** + * Sets key to use with hash + * + * @param string $key + * @return void + * @access public + */ + function setKey($key) + { + /* + * Pad the key as the RFC wishes + */ + $func = $this->_func; + + if (strlen($key) > 64) { + $key = pack($this->_pack, $func($key)); + } + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + + /* Calculate the padded keys and save them */ + $this->_ipad = (substr($key, 0, 64) ^ str_repeat(chr(0x36), 64)); + $this->_opad = (substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64)); + } + + + /** + * Gets pack formats for specifed hash function + * + * @param string $func + * @return mixed false if hash function doesnt exist, pack format on success + * @access private + */ + function _getPackFormat($func) + { + $packs = array('md5' => 'H32', 'sha1' => 'H40'); + return isset($packs[$func]) ? $packs[$func] : false; + } + + + /** + * Hashing function + * + * @param string $data string that will encrypted + * @return string + * @access public + */ + function hash($data) + { + $func = $this->_func; + return $func($this->_opad . pack($this->_pack, $func($this->_ipad . $data))); + } + +} + + +?> diff --git a/Date.php b/Date.php new file mode 100644 index 0000000..99e42c2 --- /dev/null +++ b/Date.php @@ -0,0 +1,1465 @@ + + * @author Pierre-Alain Joye + * @author Firman Wandayandi + * @copyright 1997-2006 Baba Buehler, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: Date.php,v 1.41 2006/11/22 00:28:03 firman Exp $ + * @link http://pear.php.net/package/Date + */ + +// }}} + +// {{{ Includes + +/** + * Load Date_TimeZone. + */ +require_once 'Date/TimeZone.php'; + +/** + * Load Date_Calc. + */ +require_once 'Date/Calc.php'; + +/** + * Load Date_Span. + */ +require_once 'Date/Span.php'; + +// }}} +// {{{ Constants + +// {{{ Output formats Pass this to getDate(). + +/** + * "YYYY-MM-DD HH:MM:SS" + */ +define('DATE_FORMAT_ISO', 1); + +/** + * "YYYYMMSSTHHMMSS(Z|(+/-)HHMM)?" + */ +define('DATE_FORMAT_ISO_BASIC', 2); + +/** + * "YYYY-MM-SSTHH:MM:SS(Z|(+/-)HH:MM)?" + */ +define('DATE_FORMAT_ISO_EXTENDED', 3); + +/** + * "YYYY-MM-SSTHH:MM:SS(.S*)?(Z|(+/-)HH:MM)?" + */ +define('DATE_FORMAT_ISO_EXTENDED_MICROTIME', 6); + +/** + * "YYYYMMDDHHMMSS" + */ +define('DATE_FORMAT_TIMESTAMP', 4); + +/** + * long int, seconds since the unix epoch + */ +define('DATE_FORMAT_UNIXTIME', 5); + +// }}} + +// }}} +// {{{ Class: Date + +/** + * Generic date handling class for PEAR + * + * Generic date handling class for PEAR. Attempts to be time zone aware + * through the Date::TimeZone class. Supports several operations from + * Date::Calc on Date objects. + * + * @author Baba Buehler + * @author Pierre-Alain Joye + * @author Firman Wandayandi + * @copyright 1997-2006 Baba Buehler, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.4.7 + * @link http://pear.php.net/package/Date + */ +class Date +{ + // {{{ Properties + + /** + * the year + * @var int + */ + var $year; + + /** + * the month + * @var int + */ + var $month; + + /** + * the day + * @var int + */ + var $day; + + /** + * the hour + * @var int + */ + var $hour; + + /** + * the minute + * @var int + */ + var $minute; + + /** + * the second + * @var int + */ + var $second; + + /** + * the parts of a second + * @var float + */ + var $partsecond; + + /** + * timezone for this date + * @var object Date_TimeZone + */ + var $tz; + + /** + * define the default weekday abbreviation length + * used by ::format() + * @var int + */ + var $getWeekdayAbbrnameLength = 3; + + // }}} + // {{{ Constructor + + /** + * Constructor + * + * Creates a new Date Object initialized to the current date/time in the + * system-default timezone by default. A date optionally + * passed in may be in the ISO 8601, TIMESTAMP or UNIXTIME format, + * or another Date object. If no date is passed, the current date/time + * is used. + * + * @access public + * @see setDate() + * @param mixed $date optional - date/time to initialize + * @return object Date the new Date object + */ + function Date($date = null) + { + $this->tz = Date_TimeZone::getDefault(); + if (is_null($date)) { + $this->setDate(date("Y-m-d H:i:s")); + } elseif (is_a($date, 'Date')) { + $this->copy($date); + } else { + $this->setDate($date); + } + } + + // }}} + // {{{ setDate() + + /** + * Set the fields of a Date object based on the input date and format + * + * Set the fields of a Date object based on the input date and format, + * which is specified by the DATE_FORMAT_* constants. + * + * @access public + * @param string $date input date + * @param int $format Optional format constant (DATE_FORMAT_*) of the input date. + * This parameter isn't really needed anymore, but you could + * use it to force DATE_FORMAT_UNIXTIME. + */ + function setDate($date, $format = DATE_FORMAT_ISO) + { + if ( + preg_match('/^(\d{4})-?(\d{2})-?(\d{2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?$/i', $date, $regs) + && $format != DATE_FORMAT_UNIXTIME) { + // DATE_FORMAT_ISO, ISO_BASIC, ISO_EXTENDED, and TIMESTAMP + // These formats are extremely close to each other. This regex + // is very loose and accepts almost any butchered format you could + // throw at it. e.g. 2003-10-07 19:45:15 and 2003-10071945:15 + // are the same thing in the eyes of this regex, even though the + // latter is not a valid ISO 8601 date. + $this->year = $regs[1]; + $this->month = $regs[2]; + $this->day = $regs[3]; + $this->hour = isset($regs[5])?$regs[5]:0; + $this->minute = isset($regs[6])?$regs[6]:0; + $this->second = isset($regs[7])?$regs[7]:0; + $this->partsecond = isset($regs[8])?(float)$regs[8]:(float)0; + + // if an offset is defined, convert time to UTC + // Date currently can't set a timezone only by offset, + // so it has to store it as UTC + if (isset($regs[9])) { + $this->toUTCbyOffset($regs[9]); + } + } elseif (is_numeric($date)) { + // UNIXTIME + $this->setDate(date("Y-m-d H:i:s", $date)); + } else { + // unknown format + $this->year = 0; + $this->month = 1; + $this->day = 1; + $this->hour = 0; + $this->minute = 0; + $this->second = 0; + $this->partsecond = (float)0; + } + } + + // }}} + // {{{ getDate() + + /** + * Get a string (or other) representation of this date + * + * Get a string (or other) representation of this date in the + * format specified by the DATE_FORMAT_* constants. + * + * @access public + * @param int $format format constant (DATE_FORMAT_*) of the output date + * @return string the date in the requested format + */ + function getDate($format = DATE_FORMAT_ISO) + { + switch ($format) { + case DATE_FORMAT_ISO: + return $this->format("%Y-%m-%d %T"); + break; + case DATE_FORMAT_ISO_BASIC: + $format = "%Y%m%dT%H%M%S"; + if ($this->tz->getID() == 'UTC') { + $format .= "Z"; + } + return $this->format($format); + break; + case DATE_FORMAT_ISO_EXTENDED: + $format = "%Y-%m-%dT%H:%M:%S"; + if ($this->tz->getID() == 'UTC') { + $format .= "Z"; + } + return $this->format($format); + break; + case DATE_FORMAT_ISO_EXTENDED_MICROTIME: + $format = "%Y-%m-%dT%H:%M:%s"; + if ($this->tz->getID() == 'UTC') { + $format .= "Z"; + } + return $this->format($format); + break; + case DATE_FORMAT_TIMESTAMP: + return $this->format("%Y%m%d%H%M%S"); + break; + case DATE_FORMAT_UNIXTIME: + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + break; + } + } + + // }}} + // {{{ copy() + + /** + * Copy values from another Date object + * + * Makes this Date a copy of another Date object. + * + * @access public + * @param object Date $date Date to copy from + */ + function copy($date) + { + $this->year = $date->year; + $this->month = $date->month; + $this->day = $date->day; + $this->hour = $date->hour; + $this->minute = $date->minute; + $this->second = $date->second; + $this->tz = $date->tz; + } + + // }}} + // {{{ format() + + /** + * Date pretty printing, similar to strftime() + * + * Formats the date in the given format, much like + * strftime(). Most strftime() options are supported.

+ * + * formatting options:

+ * + * %a abbreviated weekday name (Sun, Mon, Tue)
+ * %A full weekday name (Sunday, Monday, Tuesday)
+ * %b abbreviated month name (Jan, Feb, Mar)
+ * %B full month name (January, February, March)
+ * %C century number (the year divided by 100 and truncated to an integer, range 00 to 99)
+ * %d day of month (range 00 to 31)
+ * %D same as "%m/%d/%y"
+ * %e day of month, single digit (range 0 to 31)
+ * %E number of days since unspecified epoch (integer, Date_Calc::dateToDays())
+ * %H hour as decimal number (00 to 23)
+ * %I hour as decimal number on 12-hour clock (01 to 12)
+ * %j day of year (range 001 to 366)
+ * %m month as decimal number (range 01 to 12)
+ * %M minute as a decimal number (00 to 59)
+ * %n newline character (\n)
+ * %O dst-corrected timezone offset expressed as "+/-HH:MM"
+ * %o raw timezone offset expressed as "+/-HH:MM"
+ * %p either 'am' or 'pm' depending on the time
+ * %P either 'AM' or 'PM' depending on the time
+ * %r time in am/pm notation, same as "%I:%M:%S %p"
+ * %R time in 24-hour notation, same as "%H:%M"
+ * %s seconds including the decimal representation smaller than one second
+ * %S seconds as a decimal number (00 to 59)
+ * %t tab character (\t)
+ * %T current time, same as "%H:%M:%S"
+ * %w weekday as decimal (0 = Sunday)
+ * %U week number of current year, first sunday as first week
+ * %y year as decimal (range 00 to 99)
+ * %Y year as decimal including century (range 0000 to 9999)
+ * %% literal '%'
+ *
+ * + * @access public + * @param string format the format string for returned date/time + * @return string date/time in given format + */ + function format($format) + { + $output = ""; + + for($strpos = 0; $strpos < strlen($format); $strpos++) { + $char = substr($format,$strpos,1); + if ($char == "%") { + $nextchar = substr($format,$strpos + 1,1); + switch ($nextchar) { + case "a": + $output .= Date_Calc::getWeekdayAbbrname($this->day,$this->month,$this->year, $this->getWeekdayAbbrnameLength); + break; + case "A": + $output .= Date_Calc::getWeekdayFullname($this->day,$this->month,$this->year); + break; + case "b": + $output .= Date_Calc::getMonthAbbrname($this->month); + break; + case "B": + $output .= Date_Calc::getMonthFullname($this->month); + break; + case "C": + $output .= sprintf("%02d",intval($this->year/100)); + break; + case "d": + $output .= sprintf("%02d",$this->day); + break; + case "D": + $output .= sprintf("%02d/%02d/%02d",$this->month,$this->day,$this->year); + break; + case "e": + $output .= $this->day * 1; // get rid of leading zero + break; + case "E": + $output .= Date_Calc::dateToDays($this->day,$this->month,$this->year); + break; + case "H": + $output .= sprintf("%02d", $this->hour); + break; + case 'h': + $output .= sprintf("%d", $this->hour); + break; + case "I": + $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour; + $output .= sprintf("%02d", $hour==0 ? 12 : $hour); + break; + case "i": + $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour; + $output .= sprintf("%d", $hour==0 ? 12 : $hour); + break; + case "j": + $output .= Date_Calc::julianDate($this->day,$this->month,$this->year); + break; + case "m": + $output .= sprintf("%02d",$this->month); + break; + case "M": + $output .= sprintf("%02d",$this->minute); + break; + case "n": + $output .= "\n"; + break; + case "O": + $offms = $this->tz->getOffset($this); + $direction = $offms >= 0 ? "+" : "-"; + $offmins = abs($offms) / 1000 / 60; + $hours = $offmins / 60; + $minutes = $offmins % 60; + $output .= sprintf("%s%02d:%02d", $direction, $hours, $minutes); + break; + case "o": + $offms = $this->tz->getRawOffset($this); + $direction = $offms >= 0 ? "+" : "-"; + $offmins = abs($offms) / 1000 / 60; + $hours = $offmins / 60; + $minutes = $offmins % 60; + $output .= sprintf("%s%02d:%02d", $direction, $hours, $minutes); + break; + case "p": + $output .= $this->hour >= 12 ? "pm" : "am"; + break; + case "P": + $output .= $this->hour >= 12 ? "PM" : "AM"; + break; + case "r": + $hour = ($this->hour + 1) > 12 ? $this->hour - 12 : $this->hour; + $output .= sprintf("%02d:%02d:%02d %s", $hour==0 ? 12 : $hour, $this->minute, $this->second, $this->hour >= 12 ? "PM" : "AM"); + break; + case "R": + $output .= sprintf("%02d:%02d", $this->hour, $this->minute); + break; + case "s": + $output .= str_replace(',', '.', sprintf("%09f", (float)((float)$this->second + $this->partsecond))); + break; + case "S": + $output .= sprintf("%02d", $this->second); + break; + case "t": + $output .= "\t"; + break; + case "T": + $output .= sprintf("%02d:%02d:%02d", $this->hour, $this->minute, $this->second); + break; + case "w": + $output .= Date_Calc::dayOfWeek($this->day,$this->month,$this->year); + break; + case "U": + $output .= Date_Calc::weekOfYear($this->day,$this->month,$this->year); + break; + case "y": + $output .= substr($this->year,2,2); + break; + case "Y": + $output .= $this->year; + break; + case "Z": + $output .= $this->tz->inDaylightTime($this) ? $this->tz->getDSTShortName() : $this->tz->getShortName(); + break; + case "%": + $output .= "%"; + break; + default: + $output .= $char.$nextchar; + } + $strpos++; + } else { + $output .= $char; + } + } + return $output; + + } + + // }}} + // {{{ getTime() + + /** + * Get this date/time in Unix time() format + * + * Get a representation of this date in Unix time() format. This may only be + * valid for dates from 1970 to ~2038. + * + * @access public + * @return int number of seconds since the unix epoch + */ + function getTime() + { + return $this->getDate(DATE_FORMAT_UNIXTIME); + } + + // }}} + // {{{ setTZ() + + /** + * Sets the time zone of this Date + * + * Sets the time zone of this date with the given + * Date_TimeZone object. Does not alter the date/time, + * only assigns a new time zone. For conversion, use + * convertTZ(). + * + * @access public + * @param object Date_TimeZone $tz the Date_TimeZone object to use, if called + * with a paramater that is not a Date_TimeZone object, will fall through to + * setTZbyID(). + */ + function setTZ($tz) + { + if(is_a($tz, 'Date_Timezone')) { + $this->tz = $tz; + } else { + $this->setTZbyID($tz); + } + } + + // }}} + // {{{ setTZbyID() + + /** + * Sets the time zone of this date with the given time zone id + * + * Sets the time zone of this date with the given + * time zone id, or to the system default if the + * given id is invalid. Does not alter the date/time, + * only assigns a new time zone. For conversion, use + * convertTZ(). + * + * @access public + * @param string id a time zone id + */ + function setTZbyID($id) + { + if (Date_TimeZone::isValidID($id)) { + $this->tz = new Date_TimeZone($id); + } else { + $this->tz = Date_TimeZone::getDefault(); + } + } + + // }}} + // {{{ inDaylightTime() + + /** + * Tests if this date/time is in DST + * + * Returns true if daylight savings time is in effect for + * this date in this date's time zone. See Date_TimeZone::inDaylightTime() + * for compatability information. + * + * @access public + * @return boolean true if DST is in effect for this date + */ + function inDaylightTime() + { + return $this->tz->inDaylightTime($this); + } + + // }}} + // {{{ toUTC() + + /** + * Converts this date to UTC and sets this date's timezone to UTC + * + * Converts this date to UTC and sets this date's timezone to UTC + * + * @access public + */ + function toUTC() + { + if ($this->tz->getOffset($this) > 0) { + $this->subtractSeconds(intval($this->tz->getOffset($this) / 1000)); + } else { + $this->addSeconds(intval(abs($this->tz->getOffset($this)) / 1000)); + } + $this->tz = new Date_TimeZone('UTC'); + } + + // }}} + // {{{ convertTZ() + + /** + * Converts this date to a new time zone + * + * Converts this date to a new time zone. + * WARNING: This may not work correctly if your system does not allow + * putenv() or if localtime() does not work in your environment. See + * Date::TimeZone::inDaylightTime() for more information. + * + * @access public + * @param object Date_TimeZone $tz the Date::TimeZone object for the conversion time zone + */ + function convertTZ($tz) + { + // convert to UTC + if ($this->tz->getOffset($this) > 0) { + $this->subtractSeconds(intval(abs($this->tz->getOffset($this)) / 1000)); + } else { + $this->addSeconds(intval(abs($this->tz->getOffset($this)) / 1000)); + } + // convert UTC to new timezone + if ($tz->getOffset($this) > 0) { + $this->addSeconds(intval(abs($tz->getOffset($this)) / 1000)); + } else { + $this->subtractSeconds(intval(abs($tz->getOffset($this)) / 1000)); + } + $this->tz = $tz; + } + + // }}} + // {{{ convertTZbyID() + + /** + * Converts this date to a new time zone, given a valid time zone ID + * + * Converts this date to a new time zone, given a valid time zone ID + * WARNING: This may not work correctly if your system does not allow + * putenv() or if localtime() does not work in your environment. See + * Date::TimeZone::inDaylightTime() for more information. + * + * @access public + * @param string id a time zone id + */ + function convertTZbyID($id) + { + if (Date_TimeZone::isValidID($id)) { + $tz = new Date_TimeZone($id); + } else { + $tz = Date_TimeZone::getDefault(); + } + $this->convertTZ($tz); + } + + // }}} + // {{{ toUTCbyOffset() + + function toUTCbyOffset($offset) + { + if ($offset == "Z" || $offset == "+00:00" || $offset == "+0000") { + $this->toUTC(); + return true; + } + + if (preg_match('/([\+\-])(\d{2}):?(\d{2})/', $offset, $regs)) { + // convert offset to seconds + $hours = (int) isset($regs[2])?$regs[2]:0; + $mins = (int) isset($regs[3])?$regs[3]:0; + $offset = ($hours * 3600) + ($mins * 60); + + if (isset($regs[1]) && $regs[1] == "-") { + $offset *= -1; + } + + if ($offset > 0) { + $this->subtractSeconds(intval($offset)); + } else { + $this->addSeconds(intval(abs($offset))); + } + + $this->tz = new Date_TimeZone('UTC'); + return true; + } + + return false; + } + + // }}} + // {{{ addSeconds() + + /** + * Adds a given number of seconds to the date + * + * Adds a given number of seconds to the date + * + * @access public + * @param int $sec the number of seconds to add + */ + function addSeconds($sec) + { + settype($sec, 'int'); + + // Negative value given. + if ($sec < 0) { + $this->subtractSeconds(abs($sec)); + return; + } + + $this->addSpan(new Date_Span($sec)); + } + + // }}} + // {{{ addSpan() + + /** + * Adds a time span to the date + * + * Adds a time span to the date + * + * @access public + * @param object Date_Span $span the time span to add + */ + function addSpan($span) + { + if (!is_a($span, 'Date_Span')) { + return; + } + + $this->second += $span->second; + if ($this->second >= 60) { + $this->minute++; + $this->second -= 60; + } + + $this->minute += $span->minute; + if ($this->minute >= 60) { + $this->hour++; + if ($this->hour >= 24) { + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::nextDay($this->day, $this->month, $this->year), "%04s%02s%02s"); + $this->hour -= 24; + } + $this->minute -= 60; + } + + $this->hour += $span->hour; + if ($this->hour >= 24) { + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::nextDay($this->day, $this->month, $this->year), "%04s%02s%02s"); + $this->hour -= 24; + } + + $d = Date_Calc::dateToDays($this->day, $this->month, $this->year); + $d += $span->day; + + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::daysToDate($d), "%04s%02s%02s"); + $this->year = intval($this->year); + $this->month = intval($this->month); + $this->day = intval($this->day); + } + + // }}} + // {{{ subtractSeconds() + + /** + * Subtracts a given number of seconds from the date + * + * Subtracts a given number of seconds from the date + * + * @access public + * @param int $sec the number of seconds to subtract + */ + function subtractSeconds($sec) + { + settype($sec, 'int'); + + // Negative value given. + if ($sec < 0) { + $this->addSeconds(abs($sec)); + return; + } + + $this->subtractSpan(new Date_Span($sec)); + } + + // }}} + // {{{ subtractSpan() + + /** + * Subtracts a time span to the date + * + * Subtracts a time span to the date + * + * @access public + * @param object Date_Span $span the time span to subtract + */ + function subtractSpan($span) + { + if (!is_a($span, 'Date_Span')) { + return; + } + if ($span->isEmpty()) { + return; + } + + $this->second -= $span->second; + if ($this->second < 0) { + $this->minute--; + $this->second += 60; + } + + $this->minute -= $span->minute; + if ($this->minute < 0) { + $this->hour--; + if ($this->hour < 0) { + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::prevDay($this->day, $this->month, $this->year), "%04s%02s%02s"); + $this->hour += 24; + } + $this->minute += 60; + } + + $this->hour -= $span->hour; + if ($this->hour < 0) { + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::prevDay($this->day, $this->month, $this->year), "%04s%02s%02s"); + $this->hour += 24; + } + + $d = Date_Calc::dateToDays($this->day, $this->month, $this->year); + $d -= $span->day; + + list($this->year, $this->month, $this->day) = + sscanf(Date_Calc::daysToDate($d), "%04s%02s%02s"); + $this->year = intval($this->year); + $this->month = intval($this->month); + $this->day = intval($this->day); + } + + // }}} + // {{{ compare() + + /** + * Compares two dates + * + * Compares two dates. Suitable for use + * in sorting functions. + * + * @access public + * @param object Date $d1 the first date + * @param object Date $d2 the second date + * @return int 0 if the dates are equal, -1 if d1 is before d2, 1 if d1 is after d2 + */ + function compare($d1, $d2) + { + $d1->convertTZ(new Date_TimeZone('UTC')); + $d2->convertTZ(new Date_TimeZone('UTC')); + $days1 = Date_Calc::dateToDays($d1->day, $d1->month, $d1->year); + $days2 = Date_Calc::dateToDays($d2->day, $d2->month, $d2->year); + if ($days1 < $days2) return -1; + if ($days1 > $days2) return 1; + if ($d1->hour < $d2->hour) return -1; + if ($d1->hour > $d2->hour) return 1; + if ($d1->minute < $d2->minute) return -1; + if ($d1->minute > $d2->minute) return 1; + if ($d1->second < $d2->second) return -1; + if ($d1->second > $d2->second) return 1; + return 0; + } + + // }}} + // {{{ before() + + /** + * Test if this date/time is before a certain date/time + * + * Test if this date/time is before a certain date/time + * + * @access public + * @param object Date $when the date to test against + * @return boolean true if this date is before $when + */ + function before($when) + { + if (Date::compare($this,$when) == -1) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ after() + + /** + * Test if this date/time is after a certian date/time + * + * Test if this date/time is after a certian date/time + * + * @access public + * @param object Date $when the date to test against + * @return boolean true if this date is after $when + */ + function after($when) + { + if (Date::compare($this,$when) == 1) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ equals() + + /** + * Test if this date/time is exactly equal to a certian date/time + * + * Test if this date/time is exactly equal to a certian date/time + * + * @access public + * @param object Date $when the date to test against + * @return boolean true if this date is exactly equal to $when + */ + function equals($when) + { + if (Date::compare($this,$when) == 0) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ isFuture() + + /** + * Determine if this date is in the future + * + * Determine if this date is in the future + * + * @access public + * @return boolean true if this date is in the future + */ + function isFuture() + { + $now = new Date(); + if ($this->after($now)) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ isPast() + + /** + * Determine if this date is in the past + * + * Determine if this date is in the past + * + * @access public + * @return boolean true if this date is in the past + */ + function isPast() + { + $now = new Date(); + if ($this->before($now)) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ isLeapYear() + + /** + * Determine if the year in this date is a leap year + * + * Determine if the year in this date is a leap year + * + * @access public + * @return boolean true if this year is a leap year + */ + function isLeapYear() + { + return Date_Calc::isLeapYear($this->year); + } + + // }}} + // {{{ getJulianDate() + + /** + * Get the Julian date for this date + * + * Get the Julian date for this date + * + * @access public + * @return int the Julian date + */ + function getJulianDate() + { + return Date_Calc::julianDate($this->day, $this->month, $this->year); + } + + // }}} + // {{{ getDayOfWeek() + + /** + * Gets the day of the week for this date + * + * Gets the day of the week for this date (0=Sunday) + * + * @access public + * @return int the day of the week (0=Sunday) + */ + function getDayOfWeek() + { + return Date_Calc::dayOfWeek($this->day, $this->month, $this->year); + } + + // }}} + // {{{ getWeekOfYear() + + /** + * Gets the week of the year for this date + * + * Gets the week of the year for this date + * + * @access public + * @return int the week of the year + */ + function getWeekOfYear() + { + return Date_Calc::weekOfYear($this->day, $this->month, $this->year); + } + + // }}} + // {{{ getQuarterOfYear() + + /** + * Gets the quarter of the year for this date + * + * Gets the quarter of the year for this date + * + * @access public + * @return int the quarter of the year (1-4) + */ + function getQuarterOfYear() + { + return Date_Calc::quarterOfYear($this->day, $this->month, $this->year); + } + + // }}} + // {{{ getDaysInMonth() + + /** + * Gets number of days in the month for this date + * + * Gets number of days in the month for this date + * + * @access public + * @return int number of days in this month + */ + function getDaysInMonth() + { + return Date_Calc::daysInMonth($this->month, $this->year); + } + + // }}} + // {{{ getWeeksInMonth() + + /** + * Gets the number of weeks in the month for this date + * + * Gets the number of weeks in the month for this date + * + * @access public + * @return int number of weeks in this month + */ + function getWeeksInMonth() + { + return Date_Calc::weeksInMonth($this->month, $this->year); + } + + // }}} + // {{{ getDayName() + + /** + * Gets the full name or abbriviated name of this weekday + * + * Gets the full name or abbriviated name of this weekday + * + * @access public + * @param boolean $abbr abbrivate the name + * @return string name of this day + */ + function getDayName($abbr = false, $length = 3) + { + if ($abbr) { + return Date_Calc::getWeekdayAbbrname($this->day, $this->month, $this->year, $length); + } else { + return Date_Calc::getWeekdayFullname($this->day, $this->month, $this->year); + } + } + + // }}} + // {{{ getMonthName() + + /** + * Gets the full name or abbriviated name of this month + * + * Gets the full name or abbriviated name of this month + * + * @access public + * @param boolean $abbr abbrivate the name + * @return string name of this month + */ + function getMonthName($abbr = false) + { + if ($abbr) { + return Date_Calc::getMonthAbbrname($this->month); + } else { + return Date_Calc::getMonthFullname($this->month); + } + } + + // }}} + // {{{ getNextDay() + + /** + * Get a Date object for the day after this one + * + * Get a Date object for the day after this one. + * The time of the returned Date object is the same as this time. + * + * @access public + * @return object Date Date representing the next day + */ + function getNextDay() + { + $day = Date_Calc::nextDay($this->day, $this->month, $this->year, "%Y-%m-%d"); + $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second); + $newDate = new Date(); + $newDate->setDate($date); + return $newDate; + } + + // }}} + // {{{ getPrevDay() + + /** + * Get a Date object for the day before this one + * + * Get a Date object for the day before this one. + * The time of the returned Date object is the same as this time. + * + * @access public + * @return object Date Date representing the previous day + */ + function getPrevDay() + { + $day = Date_Calc::prevDay($this->day, $this->month, $this->year, "%Y-%m-%d"); + $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second); + $newDate = new Date(); + $newDate->setDate($date); + return $newDate; + } + + // }}} + // {{{ getNextWeekday() + + /** + * Get a Date object for the weekday after this one + * + * Get a Date object for the weekday after this one. + * The time of the returned Date object is the same as this time. + * + * @access public + * @return object Date Date representing the next weekday + */ + function getNextWeekday() + { + $day = Date_Calc::nextWeekday($this->day, $this->month, $this->year, "%Y-%m-%d"); + $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second); + $newDate = new Date(); + $newDate->setDate($date); + return $newDate; + } + + // }}} + // {{{ getPrevWeekday() + + /** + * Get a Date object for the weekday before this one + * + * Get a Date object for the weekday before this one. + * The time of the returned Date object is the same as this time. + * + * @access public + * @return object Date Date representing the previous weekday + */ + function getPrevWeekday() + { + $day = Date_Calc::prevWeekday($this->day, $this->month, $this->year, "%Y-%m-%d"); + $date = sprintf("%s %02d:%02d:%02d", $day, $this->hour, $this->minute, $this->second); + $newDate = new Date(); + $newDate->setDate($date); + return $newDate; + } + + // }}} + // {{{ getYear() + + /** + * Returns the year field of the date object + * + * Returns the year field of the date object + * + * @access public + * @return int the year + */ + function getYear() + { + return (int)$this->year; + } + + // }}} + // {{{ getMonth() + + /** + * Returns the month field of the date object + * + * Returns the month field of the date object + * + * @access public + * @return int the month + */ + function getMonth() + { + return (int)$this->month; + } + + // }}} + // {{{ getDay() + + /** + * Returns the day field of the date object + * + * Returns the day field of the date object + * + * @access public + * @return int the day + */ + function getDay() + { + return (int)$this->day; + } + + // }}} + // {{{ getHour() + + /** + * Returns the hour field of the date object + * + * Returns the hour field of the date object + * + * @access public + * @return int the hour + */ + function getHour() + { + return $this->hour; + } + + // }}} + // {{{ getMinute() + + /** + * Returns the minute field of the date object + * + * Returns the minute field of the date object + * + * @access public + * @return int the minute + */ + function getMinute() + { + return $this->minute; + } + + // }}} + // {{{ getSecond() + + /** + * Returns the second field of the date object + * + * Returns the second field of the date object + * + * @access public + * @return int the second + */ + function getSecond() + { + return $this->second; + } + + // }}} + // {{{ setYear() + + /** + * Set the year field of the date object + * + * Set the year field of the date object, invalid years (not 0-9999) are set to 0. + * + * @access public + * @param int $y the year + */ + function setYear($y) + { + if ($y < 0 || $y > 9999) { + $this->year = 0; + } else { + $this->year = $y; + } + } + + // }}} + // {{{ setMonth() + + /** + * Set the month field of the date object + * + * Set the month field of the date object, invalid months (not 1-12) are set to 1. + * + * @access public + * @param int $m the month + */ + function setMonth($m) + { + if ($m < 1 || $m > 12) { + $this->month = 1; + } else { + $this->month = $m; + } + } + + // }}} + // {{{ setDay() + + /** + * Set the day field of the date object + * + * Set the day field of the date object, invalid days (not 1-31) are set to 1. + * + * @access public + * @param int $d the day + */ + function setDay($d) + { + if ($d > 31 || $d < 1) { + $this->day = 1; + } else { + $this->day = $d; + } + } + + // }}} + // {{{ setHour() + + /** + * Set the hour field of the date object + * + * Set the hour field of the date object in 24-hour format. + * Invalid hours (not 0-23) are set to 0. + * + * @access public + * @param int $h the hour + */ + function setHour($h) + { + if ($h > 23 || $h < 0) { + $this->hour = 0; + } else { + $this->hour = $h; + } + } + + // }}} + // {{{ setMinute() + + /** + * Set the minute field of the date object + * + * Set the minute field of the date object, invalid minutes (not 0-59) are set to 0. + * + * @access public + * @param int $m the minute + */ + function setMinute($m) + { + if ($m > 59 || $m < 0) { + $this->minute = 0; + } else { + $this->minute = $m; + } + } + + // }}} + // {{{ setSecond() + + /** + * Set the second field of the date object + * + * Set the second field of the date object, invalid seconds (not 0-59) are set to 0. + * + * @access public + * @param int $s the second + */ + function setSecond($s) { + if ($s > 59 || $s < 0) { + $this->second = 0; + } else { + $this->second = $s; + } + } + + // }}} +} + +// }}} + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ +?> \ No newline at end of file diff --git a/Date/Calc.php b/Date/Calc.php new file mode 100644 index 0000000..43a261d --- /dev/null +++ b/Date/Calc.php @@ -0,0 +1,2117 @@ + + * @author Pierre-Alain Joye + * @author Daniel Convissor + * @copyright 1999-2006 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: Calc.php,v 1.35 2006/11/21 23:01:13 firman Exp $ + * @link http://pear.php.net/package/Date + * @since File available since Release 1.2 + */ + +// }}} + +if (!defined('DATE_CALC_BEGIN_WEEKDAY')) { + /** + * Defines what day starts the week + * + * Monday (1) is the international standard. + * Redefine this to 0 if you want weeks to begin on Sunday. + */ + define('DATE_CALC_BEGIN_WEEKDAY', 1); +} + +if (!defined('DATE_CALC_FORMAT')) { + /** + * The default value for each method's $format parameter + * + * The default is '%Y%m%d'. To override this default, define + * this constant before including Calc.php. + * + * @since Constant available since Release 1.4.4 + */ + define('DATE_CALC_FORMAT', '%Y%m%d'); +} + +// {{{ Class: Date_Calc + +/** + * Calculates, manipulates and retrieves dates + * + * It does not rely on 32-bit system time stamps, so it works dates + * before 1970 and after 2038. + * + * @author Monte Ohrt + * @author Daniel Convissor + * @copyright 1999-2006 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.4.7 + * @link http://pear.php.net/package/Date + * @since Class available since Release 1.2 + */ +class Date_Calc +{ + // {{{ dateFormat() + + /** + * Formats the date in the given format, much like strfmt() + * + * This function is used to alleviate the problem with 32-bit numbers for + * dates pre 1970 or post 2038, as strfmt() has on most systems. + * Most of the formatting options are compatible. + * + * Formatting options: + *
+     * %a   abbreviated weekday name (Sun, Mon, Tue)
+     * %A   full weekday name (Sunday, Monday, Tuesday)
+     * %b   abbreviated month name (Jan, Feb, Mar)
+     * %B   full month name (January, February, March)
+     * %d   day of month (range 00 to 31)
+     * %e   day of month, single digit (range 0 to 31)
+     * %E   number of days since unspecified epoch (integer)
+     *        (%E is useful for passing a date in a URL as
+     *        an integer value. Then simply use
+     *        daysToDate() to convert back to a date.)
+     * %j   day of year (range 001 to 366)
+     * %m   month as decimal number (range 1 to 12)
+     * %n   newline character (\n)
+     * %t   tab character (\t)
+     * %w   weekday as decimal (0 = Sunday)
+     * %U   week number of current year, first sunday as first week
+     * %y   year as decimal (range 00 to 99)
+     * %Y   year as decimal including century (range 0000 to 9999)
+     * %%   literal '%'
+     * 
+ * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * @param string $format the format string + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function dateFormat($day, $month, $year, $format) + { + if (!Date_Calc::isValidDate($day, $month, $year)) { + $year = Date_Calc::dateNow('%Y'); + $month = Date_Calc::dateNow('%m'); + $day = Date_Calc::dateNow('%d'); + } + + $output = ''; + + for ($strpos = 0; $strpos < strlen($format); $strpos++) { + $char = substr($format, $strpos, 1); + if ($char == '%') { + $nextchar = substr($format, $strpos + 1, 1); + switch($nextchar) { + case 'a': + $output .= Date_Calc::getWeekdayAbbrname($day, $month, $year); + break; + case 'A': + $output .= Date_Calc::getWeekdayFullname($day, $month, $year); + break; + case 'b': + $output .= Date_Calc::getMonthAbbrname($month); + break; + case 'B': + $output .= Date_Calc::getMonthFullname($month); + break; + case 'd': + $output .= sprintf('%02d', $day); + break; + case 'e': + $output .= $day; + break; + case 'E': + $output .= Date_Calc::dateToDays($day, $month, $year); + break; + case 'j': + $output .= Date_Calc::julianDate($day, $month, $year); + break; + case 'm': + $output .= sprintf('%02d', $month); + break; + case 'n': + $output .= "\n"; + break; + case 't': + $output .= "\t"; + break; + case 'w': + $output .= Date_Calc::dayOfWeek($day, $month, $year); + break; + case 'U': + $output .= Date_Calc::weekOfYear($day, $month, $year); + break; + case 'y': + $output .= substr($year, 2, 2); + break; + case 'Y': + $output .= $year; + break; + case '%': + $output .= '%'; + break; + default: + $output .= $char.$nextchar; + } + $strpos++; + } else { + $output .= $char; + } + } + return $output; + } + + // }}} + // {{{ defaultCentury() + + /** + * Turns a two digit year into a four digit year + * + * From '51 to '99 is in the 1900's, otherwise it's in the 2000's. + * + * @param int $year the 2 digit year + * + * @return string the 4 digit year + * + * @access public + * @static + */ + function defaultCentury($year) + { + if (strlen($year) == 1) { + $year = '0' . $year; + } + if ($year > 50) { + return '19' . $year; + } else { + return '20' . $year; + } + } + + // }}} + // {{{ dateToDays() + + /** + * Converts a date to number of days since a distant unspecified epoch + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return integer the number of days since the Date_Calc epoch + * + * @access public + * @static + */ + function dateToDays($day, $month, $year) + { + $century = (int)substr($year, 0, 2); + $year = (int)substr($year, 2, 2); + if ($month > 2) { + $month -= 3; + } else { + $month += 9; + if ($year) { + $year--; + } else { + $year = 99; + $century --; + } + } + + return (floor((146097 * $century) / 4 ) + + floor((1461 * $year) / 4 ) + + floor((153 * $month + 2) / 5 ) + + $day + 1721119); + } + + // }}} + // {{{ daysToDate() + + /** + * Converts number of days to a distant unspecified epoch + * + * @param int $days the number of days since the Date_Calc epoch + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function daysToDate($days, $format = DATE_CALC_FORMAT) + { + $days -= 1721119; + $century = floor((4 * $days - 1) / 146097); + $days = floor(4 * $days - 1 - 146097 * $century); + $day = floor($days / 4); + + $year = floor((4 * $day + 3) / 1461); + $day = floor(4 * $day + 3 - 1461 * $year); + $day = floor(($day + 4) / 4); + + $month = floor((5 * $day - 3) / 153); + $day = floor(5 * $day - 3 - 153 * $month); + $day = floor(($day + 5) / 5); + + if ($month < 10) { + $month +=3; + } else { + $month -=9; + if ($year++ == 99) { + $year = 0; + $century++; + } + } + + $century = sprintf('%02d', $century); + $year = sprintf('%02d', $year); + return Date_Calc::dateFormat($day, $month, $century . $year, $format); + } + + // }}} + // {{{ gregorianToISO() + + /** + * Converts from Gregorian Year-Month-Day to ISO Year-WeekNumber-WeekDay + * + * Uses ISO 8601 definitions. Algorithm by Rick McCarty, 1999 at + * http://personal.ecu.edu/mccartyr/ISOwdALG.txt . + * Transcribed to PHP by Jesus M. Castagnetto. + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return string the date in ISO Year-WeekNumber-WeekDay format + * + * @access public + * @static + */ + function gregorianToISO($day, $month, $year) + { + $mnth = array (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); + $y_isleap = Date_Calc::isLeapYear($year); + $y_1_isleap = Date_Calc::isLeapYear($year - 1); + $day_of_year_number = $day + $mnth[$month - 1]; + if ($y_isleap && $month > 2) { + $day_of_year_number++; + } + // find Jan 1 weekday (monday = 1, sunday = 7) + $yy = ($year - 1) % 100; + $c = ($year - 1) - $yy; + $g = $yy + intval($yy / 4); + $jan1_weekday = 1 + intval((((($c / 100) % 4) * 5) + $g) % 7); + // weekday for year-month-day + $h = $day_of_year_number + ($jan1_weekday - 1); + $weekday = 1 + intval(($h - 1) % 7); + // find if Y M D falls in YearNumber Y-1, WeekNumber 52 or + if ($day_of_year_number <= (8 - $jan1_weekday) && $jan1_weekday > 4){ + $yearnumber = $year - 1; + if ($jan1_weekday == 5 || ($jan1_weekday == 6 && $y_1_isleap)) { + $weeknumber = 53; + } else { + $weeknumber = 52; + } + } else { + $yearnumber = $year; + } + // find if Y M D falls in YearNumber Y+1, WeekNumber 1 + if ($yearnumber == $year) { + if ($y_isleap) { + $i = 366; + } else { + $i = 365; + } + if (($i - $day_of_year_number) < (4 - $weekday)) { + $yearnumber++; + $weeknumber = 1; + } + } + // find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 + if ($yearnumber == $year) { + $j = $day_of_year_number + (7 - $weekday) + ($jan1_weekday - 1); + $weeknumber = intval($j / 7); + if ($jan1_weekday > 4) { + $weeknumber--; + } + } + // put it all together + if ($weeknumber < 10) { + $weeknumber = '0'.$weeknumber; + } + return $yearnumber . '-' . $weeknumber . '-' . $weekday; + } + + // }}} + // {{{ dateSeason() + + /** + * Determines julian date of the given season + * + * Adapted from previous work in Java by James Mark Hamilton. + * + * @param string $season the season to get the date for: VERNALEQUINOX, + * SUMMERSOLSTICE, AUTUMNALEQUINOX, + * or WINTERSOLSTICE + * @param string $year the year in four digit format. Must be between + * -1000BC and 3000AD. + * + * @return float the julian date the season starts on + * + * @author James Mark Hamilton + * @author Robert Butler + * @access public + * @static + */ + function dateSeason($season, $year = 0) + { + if ($year == '') { + $year = Date_Calc::dateNow('%Y'); + } + if (($year >= -1000) && ($year <= 1000)) { + $y = $year / 1000.0; + switch ($season) { + case 'VERNALEQUINOX': + $juliandate = (((((((-0.00071 * $y) - 0.00111) * $y) + 0.06134) * $y) + 365242.1374) * $y) + 1721139.29189; + break; + case 'SUMMERSOLSTICE': + $juliandate = (((((((0.00025 * $y) + 0.00907) * $y) - 0.05323) * $y) + 365241.72562) * $y) + 1721233.25401; + break; + case 'AUTUMNALEQUINOX': + $juliandate = (((((((0.00074 * $y) - 0.00297) * $y) - 0.11677) * $y) + 365242.49558) * $y) + 1721325.70455; + break; + case 'WINTERSOLSTICE': + default: + $juliandate = (((((((-0.00006 * $y) - 0.00933) * $y) - 0.00769) * $y) + 365242.88257) * $y) + 1721414.39987; + } + } elseif (($year > 1000) && ($year <= 3000)) { + $y = ($year - 2000) / 1000; + switch ($season) { + case 'VERNALEQUINOX': + $juliandate = (((((((-0.00057 * $y) - 0.00411) * $y) + 0.05169) * $y) + 365242.37404) * $y) + 2451623.80984; + break; + case 'SUMMERSOLSTICE': + $juliandate = (((((((-0.0003 * $y) + 0.00888) * $y) + 0.00325) * $y) + 365241.62603) * $y) + 2451716.56767; + break; + case 'AUTUMNALEQUINOX': + $juliandate = (((((((0.00078 * $y) + 0.00337) * $y) - 0.11575) * $y) + 365242.01767) * $y) + 2451810.21715; + break; + case 'WINTERSOLSTICE': + default: + $juliandate = (((((((0.00032 * $y) - 0.00823) * $y) - 0.06223) * $y) + 365242.74049) * $y) + 2451900.05952; + } + } + return $juliandate; + } + + // }}} + // {{{ dateNow() + + /** + * Returns the current local date + * + * NOTE: This function retrieves the local date using strftime(), + * which may or may not be 32-bit safe on your system. + * + * @param string $format the string indicating how to format the output + * + * @return string the current date in the specified format + * + * @access public + * @static + */ + function dateNow($format = DATE_CALC_FORMAT) + { + return strftime($format, time()); + } + + // }}} + // {{{ getYear() + + /** + * Returns the current local year in format CCYY + * + * @return string the current year in four digit format + * + * @access public + * @static + */ + function getYear() + { + return Date_Calc::dateNow('%Y'); + } + + // }}} + // {{{ getMonth() + + /** + * Returns the current local month in format MM + * + * @return string the current month in two digit format + * + * @access public + * @static + */ + function getMonth() + { + return Date_Calc::dateNow('%m'); + } + + // }}} + // {{{ getDay() + + /** + * Returns the current local day in format DD + * + * @return string the current day of the month in two digit format + * + * @access public + * @static + */ + function getDay() + { + return Date_Calc::dateNow('%d'); + } + + // }}} + // {{{ julianDate() + + /** + * Returns number of days since 31 December of year before given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the julian date for the date + * + * @access public + * @static + */ + function julianDate($day = 0, $month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); + $julian = ($days[$month - 1] + $day); + if ($month > 2 && Date_Calc::isLeapYear($year)) { + $julian++; + } + return $julian; + } + + // }}} + // {{{ getWeekdayFullname() + + /** + * Returns the full weekday name for the given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return string the full name of the day of the week + * + * @access public + * @static + */ + function getWeekdayFullname($day = 0, $month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $weekday_names = Date_Calc::getWeekDays(); + $weekday = Date_Calc::dayOfWeek($day, $month, $year); + return $weekday_names[$weekday]; + } + + // }}} + // {{{ getWeekdayAbbrname() + + /** + * Returns the abbreviated weekday name for the given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param int $length the length of abbreviation + * + * @return string the abbreviated name of the day of the week + * + * @access public + * @static + * @see Date_Calc::getWeekdayFullname() + */ + function getWeekdayAbbrname($day = 0, $month = 0, $year = 0, $length = 3) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + return substr(Date_Calc::getWeekdayFullname($day, $month, $year), + 0, $length); + } + + // }}} + // {{{ getMonthFullname() + + /** + * Returns the full month name for the given month + * + * @param int $month the month + * + * @return string the full name of the month + * + * @access public + * @static + */ + function getMonthFullname($month) + { + $month = (int)$month; + if (empty($month)) { + $month = (int)Date_Calc::dateNow('%m'); + } + $month_names = Date_Calc::getMonthNames(); + return $month_names[$month]; + } + + // }}} + // {{{ getMonthAbbrname() + + /** + * Returns the abbreviated month name for the given month + * + * @param int $month the month + * @param int $length the length of abbreviation + * + * @return string the abbreviated name of the month + * + * @access public + * @static + * @see Date_Calc::getMonthFullname + */ + function getMonthAbbrname($month, $length = 3) + { + $month = (int)$month; + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + return substr(Date_Calc::getMonthFullname($month), 0, $length); + } + + // }}} + // {{{ getMonthFromFullname() + + /** + * Returns the numeric month from the month name or an abreviation + * + * Both August and Aug would return 8. + * + * @param string $month the name of the month to examine. + * Case insensitive. + * + * @return integer the month's number + * + * @access public + * @static + */ + function getMonthFromFullName($month) + { + $month = strtolower($month); + $months = Date_Calc::getMonthNames(); + while(list($id, $name) = each($months)) { + if (ereg($month, strtolower($name))) { + return $id; + } + } + return 0; + } + + // }}} + // {{{ getMonthNames() + + /** + * Returns an array of month names + * + * Used to take advantage of the setlocale function to return + * language specific month names. + * + * TODO: cache values to some global array to avoid preformace + * hits when called more than once. + * + * @returns array an array of month names + * + * @access public + * @static + */ + function getMonthNames() + { + $months = array(); + for ($i = 1; $i < 13; $i++) { + $months[$i] = strftime('%B', mktime(0, 0, 0, $i, 1, 2001)); + } + return $months; + } + + // }}} + // {{{ getWeekDays() + + /** + * Returns an array of week days + * + * Used to take advantage of the setlocale function to + * return language specific week days. + * + * TODO: cache values to some global array to avoid preformace + * hits when called more than once. + * + * @returns array an array of week day names + * + * @access public + * @static + */ + function getWeekDays() + { + $weekdays = array(); + for ($i = 0; $i < 7; $i++) { + $weekdays[$i] = strftime('%A', mktime(0, 0, 0, 1, $i, 2001)); + } + return $weekdays; + } + + // }}} + // {{{ dayOfWeek() + + /** + * Returns day of week for given date (0 = Sunday) + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the number of the day in the week + * + * @access public + * @static + */ + function dayOfWeek($day = 0, $month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + if ($month > 2) { + $month -= 2; + } else { + $month += 10; + $year--; + } + + $day = (floor((13 * $month - 1) / 5) + + $day + ($year % 100) + + floor(($year % 100) / 4) + + floor(($year / 100) / 4) - 2 * + floor($year / 100) + 77); + + $weekday_number = $day - 7 * floor($day / 7); + return $weekday_number; + } + + // }}} + // {{{ weekOfYear() + + /** + * Returns week of the year, first Sunday is first day of first week + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the number of the week in the year + * + * @access public + * @static + */ + function weekOfYear($day = 0, $month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $iso = Date_Calc::gregorianToISO($day, $month, $year); + $parts = explode('-', $iso); + $week_number = intval($parts[1]); + return $week_number; + } + + // }}} + // {{{ quarterOfYear() + + /** + * Returns quarter of the year for given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the number of the quarter in the year + * + * @access public + * @static + */ + function quarterOfYear($day = 0, $month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $year_quarter = intval(($month - 1) / 3 + 1); + return $year_quarter; + } + + // }}} + // {{{ daysInMonth() + + /** + * Find the number of days in the given month + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the number of days the month has + * + * @access public + * @static + */ + function daysInMonth($month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + if ($year == 1582 && $month == 10) { + return 21; // October 1582 only had 1st-4th and 15th-31st + } + + if ($month == 2) { + if (Date_Calc::isLeapYear($year)) { + return 29; + } else { + return 28; + } + } elseif ($month == 4 or $month == 6 or $month == 9 or $month == 11) { + return 30; + } else { + return 31; + } + } + + // }}} + // {{{ weeksInMonth() + + /** + * Returns the number of rows on a calendar month + * + * Useful for determining the number of rows when displaying a typical + * month calendar. + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int the number of weeks the month has + * + * @access public + * @static + */ + function weeksInMonth($month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + $FDOM = Date_Calc::firstOfMonthWeekday($month, $year); + if (DATE_CALC_BEGIN_WEEKDAY==1 && $FDOM==0) { + $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY; + $weeks = 1; + } elseif (DATE_CALC_BEGIN_WEEKDAY==0 && $FDOM == 6) { + $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY; + $weeks = 1; + } else { + $first_week_days = DATE_CALC_BEGIN_WEEKDAY - $FDOM; + $weeks = 0; + } + $first_week_days %= 7; + return ceil((Date_Calc::daysInMonth($month, $year) + - $first_week_days) / 7) + $weeks; + } + + // }}} + // {{{ getCalendarWeek() + + /** + * Return an array with days in week + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return array $week[$weekday] + * + * @access public + * @static + */ + function getCalendarWeek($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $week_array = array(); + + // date for the column of week + + $curr_day = Date_Calc::beginOfWeek($day, $month, $year,'%E'); + + for ($counter = 0; $counter <= 6; $counter++) { + $week_array[$counter] = Date_Calc::daysToDate($curr_day, $format); + $curr_day++; + } + return $week_array; + } + + // }}} + // {{{ getCalendarMonth() + + /** + * Return a set of arrays to construct a calendar month for the given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return array $month[$row][$col] + * + * @access public + * @static + */ + function getCalendarMonth($month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + $month_array = array(); + + // date for the first row, first column of calendar month + if (DATE_CALC_BEGIN_WEEKDAY == 1) { + if (Date_Calc::firstOfMonthWeekday($month, $year) == 0) { + $curr_day = Date_Calc::dateToDays('01', $month, $year) - 6; + } else { + $curr_day = Date_Calc::dateToDays('01', $month, $year) + - Date_Calc::firstOfMonthWeekday($month, $year) + 1; + } + } else { + $curr_day = (Date_Calc::dateToDays('01', $month, $year) + - Date_Calc::firstOfMonthWeekday($month, $year)); + } + + // number of days in this month + $daysInMonth = Date_Calc::daysInMonth($month, $year); + + $weeksInMonth = Date_Calc::weeksInMonth($month, $year); + for ($row_counter = 0; $row_counter < $weeksInMonth; $row_counter++) { + for ($column_counter = 0; $column_counter <= 6; $column_counter++) { + $month_array[$row_counter][$column_counter] = + Date_Calc::daysToDate($curr_day , $format); + $curr_day++; + } + } + + return $month_array; + } + + // }}} + // {{{ getCalendarYear() + + /** + * Return a set of arrays to construct a calendar year for the given date + * + * @param int $year the year in four digit format, default current local year + * @param string $format the string indicating how to format the output + * + * @return array $year[$month][$row][$col] + * + * @access public + * @static + */ + function getCalendarYear($year = 0, $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + + $year_array = array(); + + for ($curr_month = 0; $curr_month <= 11; $curr_month++) { + $year_array[$curr_month] = + Date_Calc::getCalendarMonth($curr_month + 1, + $year, $format); + } + + return $year_array; + } + + // }}} + // {{{ prevDay() + + /** + * Returns date of day before given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function prevDay($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = Date_Calc::dateToDays($day, $month, $year); + return Date_Calc::daysToDate($days - 1, $format); + } + + // }}} + // {{{ nextDay() + + /** + * Returns date of day after given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function nextDay($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = Date_Calc::dateToDays($day, $month, $year); + return Date_Calc::daysToDate($days + 1, $format); + } + + // }}} + // {{{ prevWeekday() + + /** + * Returns date of the previous weekday, skipping from Monday to Friday + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function prevWeekday($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = Date_Calc::dateToDays($day, $month, $year); + if (Date_Calc::dayOfWeek($day, $month, $year) == 1) { + $days -= 3; + } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 0) { + $days -= 2; + } else { + $days -= 1; + } + return Date_Calc::daysToDate($days, $format); + } + + // }}} + // {{{ nextWeekday() + + /** + * Returns date of the next weekday of given date, skipping from + * Friday to Monday + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function nextWeekday($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = Date_Calc::dateToDays($day, $month, $year); + if (Date_Calc::dayOfWeek($day, $month, $year) == 5) { + $days += 3; + } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 6) { + $days += 2; + } else { + $days += 1; + } + return Date_Calc::daysToDate($days, $format); + } + + // }}} + // {{{ prevDayOfWeek() + + /** + * Returns date of the previous specific day of the week + * from the given date + * + * @param int day of week, 0=Sunday + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param bool $onOrBefore if true and days are same, returns current day + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function prevDayOfWeek($dow, $day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT, $onOrBefore = false) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $days = Date_Calc::dateToDays($day, $month, $year); + $curr_weekday = Date_Calc::dayOfWeek($day, $month, $year); + if ($curr_weekday == $dow) { + if (!$onOrBefore) { + $days -= 7; + } + } elseif ($curr_weekday < $dow) { + $days -= 7 - ($dow - $curr_weekday); + } else { + $days -= $curr_weekday - $dow; + } + return Date_Calc::daysToDate($days, $format); + } + + // }}} + // {{{ nextDayOfWeek() + + /** + * Returns date of the next specific day of the week + * from the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param bool $onOrAfter if true and days are same, returns current day + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function nextDayOfWeek($dow, $day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT, $onOrAfter = false) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $days = Date_Calc::dateToDays($day, $month, $year); + $curr_weekday = Date_Calc::dayOfWeek($day, $month, $year); + + if ($curr_weekday == $dow) { + if (!$onOrAfter) { + $days += 7; + } + } elseif ($curr_weekday > $dow) { + $days += 7 - ($curr_weekday - $dow); + } else { + $days += $dow - $curr_weekday; + } + + return Date_Calc::daysToDate($days, $format); + } + + // }}} + // {{{ prevDayOfWeekOnOrBefore() + + /** + * Returns date of the previous specific day of the week + * on or before the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function prevDayOfWeekOnOrBefore($dow, $day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + return Date_Calc::prevDayOfWeek($dow, $day, $month, $year, $format, + true); + } + + // }}} + // {{{ nextDayOfWeekOnOrAfter() + + /** + * Returns date of the next specific day of the week + * on or after the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function nextDayOfWeekOnOrAfter($dow, $day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + return Date_Calc::nextDayOfWeek($dow, $day, $month, $year, $format, + true); + } + + // }}} + // {{{ beginOfWeek() + + /** + * Find the month day of the beginning of week for given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function beginOfWeek($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $this_weekday = Date_Calc::dayOfWeek($day, $month, $year); + $interval = (7 - DATE_CALC_BEGIN_WEEKDAY + $this_weekday) % 7; + return Date_Calc::daysToDate(Date_Calc::dateToDays($day, $month, $year) + - $interval, $format); + } + + // }}} + // {{{ endOfWeek() + + /** + * Find the month day of the end of week for given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of following month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function endOfWeek($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + $this_weekday = Date_Calc::dayOfWeek($day, $month, $year); + $interval = (6 + DATE_CALC_BEGIN_WEEKDAY - $this_weekday) % 7; + return Date_Calc::daysToDate(Date_Calc::dateToDays($day, $month, $year) + + $interval, $format); + } + + // }}} + // {{{ beginOfPrevWeek() + + /** + * Find the month day of the beginning of week before given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function beginOfPrevWeek($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $date = Date_Calc::daysToDate(Date_Calc::dateToDays($day-7, + $month, + $year), + '%Y%m%d'); + + $prev_week_year = substr($date, 0, 4); + $prev_week_month = substr($date, 4, 2); + $prev_week_day = substr($date, 6, 2); + + return Date_Calc::beginOfWeek($prev_week_day, $prev_week_month, + $prev_week_year, $format); + } + + // }}} + // {{{ beginOfNextWeek() + + /** + * Find the month day of the beginning of week after given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function beginOfNextWeek($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $date = Date_Calc::daysToDate(Date_Calc::dateToDays($day + 7, + $month, + $year), + '%Y%m%d'); + + $next_week_year = substr($date, 0, 4); + $next_week_month = substr($date, 4, 2); + $next_week_day = substr($date, 6, 2); + + return Date_Calc::beginOfWeek($next_week_day, $next_week_month, + $next_week_year, $format); + } + + // }}} + // {{{ beginOfMonth() + + /** + * Return date of first day of month of given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfMonth($month = 0, $year = 0, $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + return Date_Calc::dateFormat('01', $month, $year, $format); + } + + // }}} + // {{{ beginOfPrevMonth() + + /** + * Returns date of the first day of previous month of given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfPrevMonth($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + if ($month > 1) { + $month--; + $day = 1; + } else { + $year--; + $month = 12; + $day = 1; + } + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + // }}} + // {{{ endOfPrevMonth() + + /** + * Returns date of the last day of previous month for given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @see Date_Calc::endOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function endOfPrevMonth($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + if ($month > 1) { + $month--; + } else { + $year--; + $month = 12; + } + $day = Date_Calc::daysInMonth($month, $year); + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + // }}} + // {{{ beginOfNextMonth() + + /** + * Returns date of begin of next month of given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfNextMonth($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + if ($month < 12) { + $month++; + $day = 1; + } else { + $year++; + $month = 1; + $day = 1; + } + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + // }}} + // {{{ endOfNextMonth() + + /** + * Returns date of the last day of next month of given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @see Date_Calc::endOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function endOfNextMonth($day = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + if ($month < 12) { + $month++; + } else { + $year++; + $month = 1; + } + $day = Date_Calc::daysInMonth($month, $year); + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + // }}} + // {{{ beginOfMonthBySpan() + + /** + * Returns date of the first day of the month in the number of months + * from the given date + * + * @param int $months the number of months from the date provided. + * Positive numbers go into the future. + * Negative numbers go into the past. + * 0 is the month presented in $month. + * @param string $month the month, default is current local month + * @param string $year the year in four digit format, default is the + * current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @since Method available since Release 1.4.4 + */ + function beginOfMonthBySpan($months = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if ($months > 0) { + // future month + $tmp_mo = $month + $months; + $month = $tmp_mo % 12; + if ($month == 0) { + $month = 12; + $year = $year + floor(($tmp_mo - 1) / 12); + } else { + $year = $year + floor($tmp_mo / 12); + } + } else { + // past or present month + $tmp_mo = $month + $months; + if ($tmp_mo > 0) { + // same year + $month = $tmp_mo; + } elseif ($tmp_mo == 0) { + // prior dec + $month = 12; + $year--; + } else { + // some time in a prior year + $month = 12 + ($tmp_mo % 12); + $year = $year + floor($tmp_mo / 12); + } + } + return Date_Calc::dateFormat(1, $month, $year, $format); + } + + // }}} + // {{{ endOfMonthBySpan() + + /** + * Returns date of the last day of the month in the number of months + * from the given date + * + * @param int $months the number of months from the date provided. + * Positive numbers go into the future. + * Negative numbers go into the past. + * 0 is the month presented in $month. + * @param string $month the month, default is current local month + * @param string $year the year in four digit format, default is the + * current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + * @since Method available since Release 1.4.4 + */ + function endOfMonthBySpan($months = 0, $month = 0, $year = 0, + $format = DATE_CALC_FORMAT) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if ($months > 0) { + // future month + $tmp_mo = $month + $months; + $month = $tmp_mo % 12; + if ($month == 0) { + $month = 12; + $year = $year + floor(($tmp_mo - 1) / 12); + } else { + $year = $year + floor($tmp_mo / 12); + } + } else { + // past or present month + $tmp_mo = $month + $months; + if ($tmp_mo > 0) { + // same year + $month = $tmp_mo; + } elseif ($tmp_mo == 0) { + // prior dec + $month = 12; + $year--; + } else { + // some time in a prior year + $month = 12 + ($tmp_mo % 12); + $year = $year + floor($tmp_mo / 12); + } + } + return Date_Calc::dateFormat(Date_Calc::daysInMonth($month, $year), + $month, $year, $format); + } + + // }}} + // {{{ firstOfMonthWeekday() + + /** + * Find the day of the week for the first of the month of given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current local year + * + * @return int number of weekday for the first day, 0=Sunday + * + * @access public + * @static + */ + function firstOfMonthWeekday($month = 0, $year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + return Date_Calc::dayOfWeek('01', $month, $year); + } + + // }}} + // {{{ NWeekdayOfMonth() + + /** + * Calculates the date of the Nth weekday of the month, + * such as the second Saturday of January 2000 + * + * @param int $week the number of the week to get + * (1 = first, etc. Also can be 'last'.) + * @param int $dow the day of the week (0 = Sunday) + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * + * @access public + * @static + */ + function NWeekdayOfMonth($week, $dow, $month, $year, + $format = DATE_CALC_FORMAT) + { + if (is_numeric($week)) { + $DOW1day = ($week - 1) * 7 + 1; + $DOW1 = Date_Calc::dayOfWeek($DOW1day, $month, $year); + $wdate = ($week - 1) * 7 + 1 + (7 + $dow - $DOW1) % 7; + if ($wdate > Date_Calc::daysInMonth($month, $year)) { + return -1; + } else { + return Date_Calc::dateFormat($wdate, $month, $year, $format); + } + } elseif ($week == 'last' && $dow < 7) { + $lastday = Date_Calc::daysInMonth($month, $year); + $lastdow = Date_Calc::dayOfWeek($lastday, $month, $year); + $diff = $dow - $lastdow; + if ($diff > 0) { + return Date_Calc::dateFormat($lastday - (7 - $diff), $month, + $year, $format); + } else { + return Date_Calc::dateFormat($lastday + $diff, $month, + $year, $format); + } + } else { + return -1; + } + } + + // }}} + // {{{ isValidDate() + + /** + * Returns true for valid date, false for invalid date + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return boolean + * + * @access public + * @static + */ + function isValidDate($day, $month, $year) + { + if ($year < 0 || $year > 9999) { + return false; + } + if (!checkdate($month, $day, $year)) { + return false; + } + return true; + } + + // }}} + // {{{ isLeapYear() + + /** + * Returns true for a leap year, else false + * + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return boolean + * + * @access public + * @static + */ + function isLeapYear($year = 0) + { + if (empty($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (preg_match('/\D/', $year)) { + return false; + } + if ($year < 1000) { + return false; + } + if ($year < 1582) { + // pre Gregorio XIII - 1582 + return ($year % 4 == 0); + } else { + // post Gregorio XIII - 1582 + return (($year % 4 == 0) && ($year % 100 != 0)) || ($year % 400 == 0); + } + } + + // }}} + // {{{ isFutureDate() + + /** + * Determines if given date is a future date from now + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return boolean + * + * @access public + * @static + */ + function isFutureDate($day, $month, $year) + { + $this_year = Date_Calc::dateNow('%Y'); + $this_month = Date_Calc::dateNow('%m'); + $this_day = Date_Calc::dateNow('%d'); + + if ($year > $this_year) { + return true; + } elseif ($year == $this_year) { + if ($month > $this_month) { + return true; + } elseif ($month == $this_month) { + if ($day > $this_day) { + return true; + } + } + } + return false; + } + + // }}} + // {{{ isPastDate() + + /** + * Determines if given date is a past date from now + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return boolean + * + * @access public + * @static + */ + function isPastDate($day, $month, $year) + { + $this_year = Date_Calc::dateNow('%Y'); + $this_month = Date_Calc::dateNow('%m'); + $this_day = Date_Calc::dateNow('%d'); + + if ($year < $this_year) { + return true; + } elseif ($year == $this_year) { + if ($month < $this_month) { + return true; + } elseif ($month == $this_month) { + if ($day < $this_day) { + return true; + } + } + } + return false; + } + + // }}} + // {{{ dateDiff() + + /** + * Returns number of days between two given dates + * + * @param int $day1 the day of the month + * @param int $month1 the month + * @param int $year1 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * @param int $day2 the day of the month + * @param int $month2 the month + * @param int $year2 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return int the absolute number of days between the two dates. + * If an error occurs, -1 is returned. + * + * @access public + * @static + */ + function dateDiff($day1, $month1, $year1, $day2, $month2, $year2) + { + if (!Date_Calc::isValidDate($day1, $month1, $year1)) { + return -1; + } + if (!Date_Calc::isValidDate($day2, $month2, $year2)) { + return -1; + } + return abs(Date_Calc::dateToDays($day1, $month1, $year1) + - Date_Calc::dateToDays($day2, $month2, $year2)); + } + + // }}} + // {{{ compareDates() + + /** + * Compares two dates + * + * @param int $day1 the day of the month + * @param int $month1 the month + * @param int $year1 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * @param int $day2 the day of the month + * @param int $month2 the month + * @param int $year2 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * Do not add leading 0's for years prior to 1000. + * + * @return int 0 if the dates are equal. 1 if date 1 is later, -1 if + * date 1 is earlier. + * + * @access public + * @static + */ + function compareDates($day1, $month1, $year1, $day2, $month2, $year2) + { + $ndays1 = Date_Calc::dateToDays($day1, $month1, $year1); + $ndays2 = Date_Calc::dateToDays($day2, $month2, $year2); + if ($ndays1 == $ndays2) { + return 0; + } + return ($ndays1 > $ndays2) ? 1 : -1; + } + + // }}} +} + +// }}} + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ +?> \ No newline at end of file diff --git a/Date/Human.php b/Date/Human.php new file mode 100644 index 0000000..4f8d3b2 --- /dev/null +++ b/Date/Human.php @@ -0,0 +1,242 @@ + + * @copyright 1997-2006 Allan Kent + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: Human.php,v 1.6 2006/11/21 17:38:15 firman Exp $ + * @link http://pear.php.net/package/Date + * @since File available since Release 1.3 + */ + +// }}} +// {{{ Class: Date_Human + +/** + * Class to convert date strings between Gregorian and Human calendar formats + * + * The Human Calendar format has been proposed by Scott Flansburg and can be + * explained as follows: + * The year is made up of 13 months + * Each month has 28 days + * Counting of months starts from 0 (zero) so the months will run from 0 to 12 + * New Years day (00) is a monthless day + * Note: Leap Years are not yet accounted for in the Human Calendar system + * + * @author Allan Kent + * @copyright 1997-2005 Allan Kent + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.4.7 + * @link http://pear.php.net/package/Date + * @since Class available since Release 1.3 + */ +class Date_Human +{ + // {{{ gregorianToHuman() + + /** + * Returns an associative array containing the converted date information + * in 'Human Calendar' format. + * + * @param int day in DD format, default current local day + * @param int month in MM format, default current local month + * @param int year in CCYY format, default to current local year + * + * @access public + * + * @return associative array( + * hdom, // Human Day Of Month, starting at 1 + * hdow, // Human Day Of Week, starting at 1 + * hwom, // Human Week of Month, starting at 1 + * hwoy, // Human Week of Year, starting at 1 + * hmoy, // Human Month of Year, starting at 0 + * ) + * + * If the day is New Years Day, the function will return + * "hdom" => 0 + * "hdow" => 0 + * "hwom" => 0 + * "hwoy" => 0 + * "hmoy" => -1 + * Since 0 is a valid month number under the Human Calendar, I have left + * the month as -1 for New Years Day. + */ + function gregorianToHuman($day=0, $month=0, $year=0) + { + /* + * Check to see if any of the arguments are empty + * If they are then populate the $dateinfo array + * Then check to see which arguments are empty and fill + * those with the current date info + */ + if ((empty($day) || (empty($month)) || empty($year))) { + $dateinfo = getdate(time()); + } + if (empty($day)) { + $day = $dateinfo["mday"]; + } + if (empty($month)) { + $month = $dateinfo["mon"]; + } + if (empty($year)) { + $year = $dateinfo["year"]; + } + /* + * We need to know how many days into the year we are + */ + $dateinfo = getdate(mktime(0, 0, 0, $month, $day, $year)); + $dayofyear = $dateinfo["yday"]; + /* + * Human Calendar starts at 0 for months and the first day of the year + * is designated 00, so we need to start our day of the year at 0 for + * these calculations. + * Also, the day of the month is calculated with a modulus of 28. + * Because a day is 28 days, the last day of the month would have a + * remainder of 0 and not 28 as it should be. Decrementing $dayofyear + * gets around this. + */ + $dayofyear--; + /* + * 28 days in a month... + */ + $humanMonthOfYear = floor($dayofyear / 28); + /* + * If we are in the first month then the day of the month is $dayofyear + * else we need to find the modulus of 28. + */ + if ($humanMonthOfYear == 0) { + $humanDayOfMonth = $dayofyear; + } else { + $humanDayOfMonth = ($dayofyear) % 28; + } + /* + * Day of the week is modulus 7 + */ + $humanDayOfWeek = $dayofyear % 7; + /* + * We can now increment $dayofyear back to it's correct value for + * the remainder of the calculations + */ + $dayofyear++; + /* + * $humanDayOfMonth needs to be incremented now - recall that we fudged + * it a bit by decrementing $dayofyear earlier + * Same goes for $humanDayOfWeek + */ + $humanDayOfMonth++; + $humanDayOfWeek++; + /* + * Week of the month is day of the month divided by 7, rounded up + * Same for week of the year, but use $dayofyear instead $humanDayOfMonth + */ + $humanWeekOfMonth = ceil($humanDayOfMonth / 7); + $humanWeekOfYear = ceil($dayofyear / 7); + /* + * Return an associative array of the values + */ + return array( + "hdom" => $humanDayOfMonth, + "hdow" => $humanDayOfWeek, + "hwom" => $humanWeekOfMonth, + "hwoy" => $humanWeekOfYear, + "hmoy" => $humanMonthOfYear ); + } + + // }}} + // {{{ humanToGregorian() + + /** + * Returns unix timestamp for a given Human Calendar date + * + * @param int day in DD format + * @param int month in MM format + * @param int year in CCYY format, default to current local year + * + * @access public + * + * @return int unix timestamp of date + */ + function humanToGregorian($day, $month, $year=0) + { + /* + * Check to see if the year has been passed through. + * If not get current year + */ + if (empty($year)) { + $dateinfo = getdate(time()); + $year = $dateinfo["year"]; + } + /* + * We need to get the day of the year that we are currently at so that + * we can work out the Gregorian Month and day + */ + $DayOfYear = $month * 28; + $DayOfYear += $day; + /* + * Human Calendar starts at 0, so we need to increment $DayOfYear + * to take into account the day 00 + */ + $DayOfYear++; + /* + * the mktime() function will correctly calculate the date for out of + * range values, so putting $DayOfYear instead of the day of the month + * will work fine. + */ + $GregorianTimeStamp = mktime(0, 0, 0, 1, $DayOfYear, $year); + return $GregorianTimeStamp; + } + + // }}} +} + +// }}} + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ +?> \ No newline at end of file diff --git a/Date/Span.php b/Date/Span.php new file mode 100644 index 0000000..32b7bab --- /dev/null +++ b/Date/Span.php @@ -0,0 +1,1083 @@ + + * @author Pierre-Alain Joye + * @copyright 1997-2006 Leandro Lucarella, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: Span.php,v 1.9 2006/11/21 17:38:15 firman Exp $ + * @link http://pear.php.net/package/Date + * @since File available since Release 1.4 + */ + +// }}} +// {{{ Includes + +/** + * Get the Date class + */ +require_once 'Date.php'; + +/** + * Get the Date_Calc class + */ +require_once 'Date/Calc.php'; + +// }}} +// {{{ Constants + +/** + * Non Numeric Separated Values (NNSV) Input Format. + * + * Input format guessed from something like this: + * dayshoursminutesseconds + * Where is any quantity of non numeric chars. If no values are + * given, time span is set to zero, if one value is given, it's used for + * hours, if two values are given it's used for hours and minutes and if + * three values are given, it's used for hours, minutes and seconds.
+ * Examples:
+ * '' -> 0, 0, 0, 0 (days, hours, minutes, seconds)
+ * '12' -> 0, 12, 0, 0 + * '12.30' -> 0, 12, 30, 0
+ * '12:30:18' -> 0, 12, 30, 18
+ * '3-12-30-18' -> 3, 12, 30, 18
+ * '3 days, 12-30-18' -> 3, 12, 30, 18
+ * '12:30 with 18 secs' -> 0, 12, 30, 18
+ * + * @const int + */ +define('DATE_SPAN_INPUT_FORMAT_NNSV', 1); + +// }}} +// {{{ Global Variables + +/** + * Default time format when converting to a string. + * + * @global string + */ +$GLOBALS['_DATE_SPAN_FORMAT'] = '%C'; + +/** + * Default time format when converting from a string. + * + * @global mixed + */ +$GLOBALS['_DATE_SPAN_INPUT_FORMAT'] = DATE_SPAN_INPUT_FORMAT_NNSV; + +// }}} +// {{{ Class: Date_Span + +/** + * Generic time span handling class for PEAR + * + * @author Leandro Lucarella + * @author Pierre-Alain Joye + * @copyright 1997-2006 Leandro Lucarella, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.4.7 + * @link http://pear.php.net/package/Date + * @since Class available since Release 1.4 + */ +class Date_Span +{ + // {{{ Properties + + /** + * @var int + */ + var $day; + + /** + * @var int + */ + var $hour; + + /** + * @var int + */ + var $minute; + + /** + * @var int + */ + var $second; + + // }}} + // {{{ Constructor + + /** + * Constructor. + * + * Creates the time span object calling the set() method. + * + * @param mixed $time Time span expression. + * @param mixed $format Format string to set it from a string or the + * second date set it from a date diff. + * + * @see set() + * @access public + */ + function Date_Span($time = 0, $format = null) + { + $this->set($time, $format); + } + + // }}} + // {{{ set() + + /** + * Set the time span to a new value in a 'smart' way. + * + * Sets the time span depending on the argument types, calling + * to the appropriate setFromXxx() method. + * + * @param mixed $time Time span expression. + * @param mixed $format Format string to set it from a string or the + * second date set it from a date diff. + * + * @return bool true on success. + * + * @see setFromObject() + * @see setFromArray() + * @see setFromString() + * @see setFromSeconds() + * @see setFromDateDiff() + * @access public + */ + function set($time = 0, $format = null) + { + if (is_a($time, 'date_span')) { + return $this->copy($time); + } elseif (is_a($time, 'date') and is_a($format, 'date')) { + return $this->setFromDateDiff($time, $format); + } elseif (is_array($time)) { + return $this->setFromArray($time); + } elseif (is_string($time)) { + return $this->setFromString($time, $format); + } elseif (is_int($time)) { + return $this->setFromSeconds($time); + } else { + return $this->setFromSeconds(0); + } + } + + // }}} + // {{{ setFromArray() + + /** + * Set the time span from an array. + * + * Set the time span from an array. Any value can be a float (but it + * has no sense in seconds), for example array(23.5, 20, 0) is + * interpreted as 23 hours, .5*60 + 20 = 50 minutes and 0 seconds. + * + * @param array $time Items are counted from right to left. First + * item is for seconds, second for minutes, third + * for hours and fourth for days. If there are + * less items than 4, zero (0) is assumed for the + * absent values. + * + * @return bool True on success. + * + * @access public + */ + function setFromArray($time) + { + if (!is_array($time)) { + return false; + } + $tmp1 = new Date_Span; + if (!$tmp1->setFromSeconds(@array_pop($time))) { + return false; + } + $tmp2 = new Date_Span; + if (!$tmp2->setFromMinutes(@array_pop($time))) { + return false; + } + $tmp1->add($tmp2); + if (!$tmp2->setFromHours(@array_pop($time))) { + return false; + } + $tmp1->add($tmp2); + if (!$tmp2->setFromDays(@array_pop($time))) { + return false; + } + $tmp1->add($tmp2); + return $this->copy($tmp1); + } + + // }}} + // {{{ setFromString() + + /** + * Set the time span from a string based on an input format. + * + * Set the time span from a string based on an input format. This is + * some like a mix of format() method and sscanf() PHP function. The + * error checking and validation of this function is very primitive, + * so you should be carefull when using it with unknown $time strings. + * With this method you are assigning day, hour, minute and second + * values, and the last values are used. This means that if you use + * something like setFromString('10, 20', '%H, %h') your time span + * would be 20 hours long. Allways remember that this method set + * all the values, so if you had a $time span 30 minutes long + * and you make $time->setFromString('20 hours', '%H hours'), $time + * span would be 20 hours long (and not 20 hours and 30 minutes). + * Input format options:
+ * %C Days with time, same as "%D, %H:%M:%S".
+ * %d Total days as a float number + * (2 days, 12 hours = 2.5 days).
+ * %D Days as a decimal number.
+ * %e Total hours as a float number + * (1 day, 2 hours, 30 minutes = 26.5 hours).
+ * %f Total minutes as a float number + * (2 minutes, 30 seconds = 2.5 minutes).
+ * %g Total seconds as a decimal number + * (2 minutes, 30 seconds = 90 seconds).
+ * %h Hours as decimal number.
+ * %H Hours as decimal number limited to 2 digits.
+ * %m Minutes as a decimal number.
+ * %M Minutes as a decimal number limited to 2 digits.
+ * %n Newline character (\n).
+ * %p Either 'am' or 'pm' depending on the time. If 'pm' + * is detected it adds 12 hours to the resulting time + * span (without any checks). This is case + * insensitive.
+ * %r Time in am/pm notation, same as "%H:%M:%S %p".
+ * %R Time in 24-hour notation, same as "%H:%M".
+ * %s Seconds as a decimal number.
+ * %S Seconds as a decimal number limited to 2 digits.
+ * %t Tab character (\t).
+ * %T Current time equivalent, same as "%H:%M:%S".
+ * %% Literal '%'.
+ * + * @param string $time String from where to get the time span + * information. + * @param string $format Format string. + * + * @return bool True on success. + * + * @access public + */ + function setFromString($time, $format = null) + { + if (is_null($format)) { + $format = $GLOBALS['_DATE_SPAN_INPUT_FORMAT']; + } + // If format is a string, it parses the string format. + if (is_string($format)) { + $str = ''; + $vars = array(); + $pm = 'am'; + $day = $hour = $minute = $second = 0; + for ($i = 0; $i < strlen($format); $i++) { + $char = $format{$i}; + if ($char == '%') { + $nextchar = $format{++$i}; + switch ($nextchar) { + case 'c': + $str .= '%d, %d:%d:%d'; + array_push( + $vars, 'day', 'hour', 'minute', 'second'); + break; + case 'C': + $str .= '%d, %2d:%2d:%2d'; + array_push( + $vars, 'day', 'hour', 'minute', 'second'); + break; + case 'd': + $str .= '%f'; + array_push($vars, 'day'); + break; + case 'D': + $str .= '%d'; + array_push($vars, 'day'); + break; + case 'e': + $str .= '%f'; + array_push($vars, 'hour'); + break; + case 'f': + $str .= '%f'; + array_push($vars, 'minute'); + break; + case 'g': + $str .= '%f'; + array_push($vars, 'second'); + break; + case 'h': + $str .= '%d'; + array_push($vars, 'hour'); + break; + case 'H': + $str .= '%2d'; + array_push($vars, 'hour'); + break; + case 'm': + $str .= '%d'; + array_push($vars, 'minute'); + break; + case 'M': + $str .= '%2d'; + array_push($vars, 'minute'); + break; + case 'n': + $str .= "\n"; + break; + case 'p': + $str .= '%2s'; + array_push($vars, 'pm'); + break; + case 'r': + $str .= '%2d:%2d:%2d %2s'; + array_push( + $vars, 'hour', 'minute', 'second', 'pm'); + break; + case 'R': + $str .= '%2d:%2d'; + array_push($vars, 'hour', 'minute'); + break; + case 's': + $str .= '%d'; + array_push($vars, 'second'); + break; + case 'S': + $str .= '%2d'; + array_push($vars, 'second'); + break; + case 't': + $str .= "\t"; + break; + case 'T': + $str .= '%2d:%2d:%2d'; + array_push($vars, 'hour', 'minute', 'second'); + break; + case '%': + $str .= "%"; + break; + default: + $str .= $char . $nextchar; + } + } else { + $str .= $char; + } + } + $vals = sscanf($time, $str); + foreach ($vals as $i => $val) { + if (is_null($val)) { + return false; + } + $$vars[$i] = $val; + } + if (strcasecmp($pm, 'pm') == 0) { + $hour += 12; + } elseif (strcasecmp($pm, 'am') != 0) { + return false; + } + $this->setFromArray(array($day, $hour, $minute, $second)); + // If format is a integer, it uses a predefined format + // detection method. + } elseif (is_integer($format)) { + switch ($format) { + case DATE_SPAN_INPUT_FORMAT_NNSV: + $time = preg_split('/\D+/', $time); + switch (count($time)) { + case 0: + return $this->setFromArray( + array(0, 0, 0, 0)); + case 1: + return $this->setFromArray( + array(0, $time[0], 0, 0)); + case 2: + return $this->setFromArray( + array(0, $time[0], $time[1], 0)); + case 3: + return $this->setFromArray( + array(0, $time[0], $time[1], $time[2])); + default: + return $this->setFromArray($time); + } + break; + } + } + return false; + } + + // }}} + // {{{ setFromSeconds() + + /** + * Set the time span from a total number of seconds. + * + * @param int $seconds Total number of seconds. + * + * @return bool True on success. + * + * @access public + */ + function setFromSeconds($seconds) + { + if ($seconds < 0) { + return false; + } + $sec = intval($seconds); + $min = floor($sec / 60); + $hour = floor($min / 60); + $day = intval(floor($hour / 24)); + $this->second = $sec % 60; + $this->minute = $min % 60; + $this->hour = $hour % 24; + $this->day = $day; + return true; + } + + // }}} + // {{{ setFromMinutes() + + /** + * Set the time span from a total number of minutes. + * + * @param float $minutes Total number of minutes. + * + * @return bool True on success. + * + * @access public + */ + function setFromMinutes($minutes) + { + return $this->setFromSeconds(round($minutes * 60)); + } + + // }}} + // {{{ setFromHours() + + /** + * Set the time span from a total number of hours. + * + * @param float $hours Total number of hours. + * + * @return bool True on success. + * + * @access public + */ + function setFromHours($hours) + { + return $this->setFromSeconds(round($hours * 3600)); + } + + // }}} + // {{{ setFromDays() + + /** + * Set the time span from a total number of days. + * + * @param float $days Total number of days. + * + * @return bool True on success. + * + * @access public + */ + function setFromDays($days) + { + return $this->setFromSeconds(round($days * 86400)); + } + + // }}} + // {{{ setFromDateDiff() + + /** + * Set the span from the elapsed time between two dates. + * + * Set the span from the elapsed time between two dates. The time span + * is allways positive, so the date's order is not important. + * + * @param object Date $date1 First Date. + * @param object Date $date2 Second Date. + * + * @return bool True on success. + * + * @access public + */ + function setFromDateDiff($date1, $date2) + { + if (!is_a($date1, 'date') or !is_a($date2, 'date')) { + return false; + } + $date1->toUTC(); + $date2->toUTC(); + if ($date1->after($date2)) { + list($date1, $date2) = array($date2, $date1); + } + $days = Date_Calc::dateDiff( + $date1->getDay(), $date1->getMonth(), $date1->getYear(), + $date2->getDay(), $date2->getMonth(), $date2->getYear() + ); + $hours = $date2->getHour() - $date1->getHour(); + $mins = $date2->getMinute() - $date1->getMinute(); + $secs = $date2->getSecond() - $date1->getSecond(); + $this->setFromSeconds( + $days * 86400 + $hours * 3600 + $mins * 60 + $secs + ); + return true; + } + + // }}} + // {{{ copy() + + /** + * Set the time span from another time object. + * + * @param object Date_Span $time Source time span object. + * + * @return bool True on success. + * + * @access public + */ + function copy($time) + { + if (is_a($time, 'date_span')) { + $this->second = $time->second; + $this->minute = $time->minute; + $this->hour = $time->hour; + $this->day = $time->day; + return true; + } else { + return false; + } + } + + // }}} + // {{{ format() + + /** + * Time span pretty printing (similar to Date::format()). + * + * Formats the time span in the given format, similar to + * strftime() and Date::format().
+ *
+ * Formatting options:
+ * %C Days with time, same as "%D, %H:%M:%S".
+ * %d Total days as a float number + * (2 days, 12 hours = 2.5 days).
+ * %D Days as a decimal number.
+ * %e Total hours as a float number + * (1 day, 2 hours, 30 minutes = 26.5 hours).
+ * %E Total hours as a decimal number + * (1 day, 2 hours, 40 minutes = 26 hours).
+ * %f Total minutes as a float number + * (2 minutes, 30 seconds = 2.5 minutes).
+ * %F Total minutes as a decimal number + * (1 hour, 2 minutes, 40 seconds = 62 minutes).
+ * %g Total seconds as a decimal number + * (2 minutes, 30 seconds = 90 seconds).
+ * %h Hours as decimal number (0 to 23).
+ * %H Hours as decimal number (00 to 23).
+ * %i Hours as decimal number on 12-hour clock + * (1 to 12).
+ * %I Hours as decimal number on 12-hour clock + * (01 to 12).
+ * %m Minutes as a decimal number (0 to 59).
+ * %M Minutes as a decimal number (00 to 59).
+ * %n Newline character (\n).
+ * %p Either 'am' or 'pm' depending on the time.
+ * %P Either 'AM' or 'PM' depending on the time.
+ * %r Time in am/pm notation, same as "%I:%M:%S %p".
+ * %R Time in 24-hour notation, same as "%H:%M".
+ * %s Seconds as a decimal number (0 to 59).
+ * %S Seconds as a decimal number (00 to 59).
+ * %t Tab character (\t).
+ * %T Current time equivalent, same as "%H:%M:%S".
+ * %% Literal '%'.
+ * + * @param string $format The format string for returned time span. + * + * @return string The time span in specified format. + * + * @access public + */ + function format($format = null) + { + if (is_null($format)) { + $format = $GLOBALS['_DATE_SPAN_FORMAT']; + } + $output = ''; + for ($i = 0; $i < strlen($format); $i++) { + $char = $format{$i}; + if ($char == '%') { + $nextchar = $format{++$i}; + switch ($nextchar) { + case 'C': + $output .= sprintf( + '%d, %02d:%02d:%02d', + $this->day, + $this->hour, + $this->minute, + $this->second + ); + break; + case 'd': + $output .= $this->toDays(); + break; + case 'D': + $output .= $this->day; + break; + case 'e': + $output .= $this->toHours(); + break; + case 'E': + $output .= floor($this->toHours()); + break; + case 'f': + $output .= $this->toMinutes(); + break; + case 'F': + $output .= floor($this->toMinutes()); + break; + case 'g': + $output .= $this->toSeconds(); + break; + case 'h': + $output .= $this->hour; + break; + case 'H': + $output .= sprintf('%02d', $this->hour); + break; + case 'i': + $hour = + ($this->hour + 1) > 12 ? + $this->hour - 12 : + $this->hour; + $output .= ($hour == 0) ? 12 : $hour; + break; + case 'I': + $hour = + ($this->hour + 1) > 12 ? + $this->hour - 12 : + $this->hour; + $output .= sprintf('%02d', $hour==0 ? 12 : $hour); + break; + case 'm': + $output .= $this->minute; + break; + case 'M': + $output .= sprintf('%02d',$this->minute); + break; + case 'n': + $output .= "\n"; + break; + case 'p': + $output .= $this->hour >= 12 ? 'pm' : 'am'; + break; + case 'P': + $output .= $this->hour >= 12 ? 'PM' : 'AM'; + break; + case 'r': + $hour = + ($this->hour + 1) > 12 ? + $this->hour - 12 : + $this->hour; + $output .= sprintf( + '%02d:%02d:%02d %s', + $hour==0 ? 12 : $hour, + $this->minute, + $this->second, + $this->hour >= 12 ? 'pm' : 'am' + ); + break; + case 'R': + $output .= sprintf( + '%02d:%02d', $this->hour, $this->minute + ); + break; + case 's': + $output .= $this->second; + break; + case 'S': + $output .= sprintf('%02d', $this->second); + break; + case 't': + $output .= "\t"; + break; + case 'T': + $output .= sprintf( + '%02d:%02d:%02d', + $this->hour, $this->minute, $this->second + ); + break; + case '%': + $output .= "%"; + break; + default: + $output .= $char . $nextchar; + } + } else { + $output .= $char; + } + } + return $output; + } + + // }}} + // {{{ toSeconds() + + /** + * Convert time span to seconds. + * + * @return int Time span as an integer number of seconds. + * + * @access public + */ + function toSeconds() + { + return $this->day * 86400 + $this->hour * 3600 + + $this->minute * 60 + $this->second; + } + + // }}} + // {{{ toMinutes() + + /** + * Convert time span to minutes. + * + * @return float Time span as a decimal number of minutes. + * + * @access public + */ + function toMinutes() + { + return $this->day * 1440 + $this->hour * 60 + $this->minute + + $this->second / 60; + } + + // }}} + // {{{ toHours() + + /** + * Convert time span to hours. + * + * @return float Time span as a decimal number of hours. + * + * @access public + */ + function toHours() + { + return $this->day * 24 + $this->hour + $this->minute / 60 + + $this->second / 3600; + } + + // }}} + // {{{ toDays() + + /** + * Convert time span to days. + * + * @return float Time span as a decimal number of days. + * + * @access public + */ + function toDays() + { + return $this->day + $this->hour / 24 + $this->minute / 1440 + + $this->second / 86400; + } + + // }}} + // {{{ add() + + /** + * Adds a time span. + * + * @param object Date_Span $time Time span to add. + * + * @access public + */ + function add($time) + { + return $this->setFromSeconds( + $this->toSeconds() + $time->toSeconds() + ); + } + + // }}} + // {{{ substract() + + /** + * Subtracts a time span. + * + * Subtracts a time span. If the time span to subtract is larger + * than the original, the result is zero (there's no sense in + * negative time spans). + * + * @param object Date_Span $time Time span to subtract. + * + * @access public + */ + function subtract($time) + { + $sub = $this->toSeconds() - $time->toSeconds(); + if ($sub < 0) { + $this->setFromSeconds(0); + } else { + $this->setFromSeconds($sub); + } + } + + // }}} + // {{{ equal() + + /** + * Tells if time span is equal to $time. + * + * @param object Date_Span $time Time span to compare to. + * + * @return bool True if the time spans are equal. + * + * @access public + */ + function equal($time) + { + return $this->toSeconds() == $time->toSeconds(); + } + + // }}} + // {{{ greaterEqual() + + /** + * Tells if this time span is greater or equal than $time. + * + * @param object Date_Span $time Time span to compare to. + * + * @return bool True if this time span is greater or equal than $time. + * + * @access public + */ + function greaterEqual($time) + { + return $this->toSeconds() >= $time->toSeconds(); + } + + // }}} + // {{{ lowerEqual() + + /** + * Tells if this time span is lower or equal than $time. + * + * @param object Date_Span $time Time span to compare to. + * + * @return bool True if this time span is lower or equal than $time. + * + * @access public + */ + function lowerEqual($time) + { + return $this->toSeconds() <= $time->toSeconds(); + } + + // }}} + // {{{ greater() + + /** + * Tells if this time span is greater than $time. + * + * @param object Date_Span $time Time span to compare to. + * + * @return bool True if this time span is greater than $time. + * + * @access public + */ + function greater($time) + { + return $this->toSeconds() > $time->toSeconds(); + } + + // }}} + // {{{ lower() + + /** + * Tells if this time span is lower than $time. + * + * @param object Date_Span $time Time span to compare to. + * + * @return bool True if this time span is lower than $time. + * + * @access public + */ + function lower($time) + { + return $this->toSeconds() < $time->toSeconds(); + } + + // }}} + // {{{ compare() + + /** + * Compares two time spans. + * + * Compares two time spans. Suitable for use in sorting functions. + * + * @param object Date_Span $time1 The first time span. + * @param object Date_Span $time2 The second time span. + * + * @return int 0 if the time spans are equal, -1 if time1 is lower + * than time2, 1 if time1 is greater than time2. + * + * @static + * @access public + */ + function compare($time1, $time2) + { + if ($time1->equal($time2)) { + return 0; + } elseif ($time1->lower($time2)) { + return -1; + } else { + return 1; + } + } + + // }}} + // {{{ isEmpty() + + /** + * Tells if the time span is empty (zero length). + * + * @return bool True is it's empty. + */ + function isEmpty() + { + return !$this->day && !$this->hour && !$this->minute && !$this->second; + } + + // }}} + // {{{ setDefaultInputFormat() + + /** + * Set the default input format. + * + * @param mixed $format New default input format. + * + * @return mixed Previous default input format. + * + * @static + */ + function setDefaultInputFormat($format) + { + $old = $GLOBALS['_DATE_SPAN_INPUT_FORMAT']; + $GLOBALS['_DATE_SPAN_INPUT_FORMAT'] = $format; + return $old; + } + + // }}} + // {{{ getDefaultInputFormat() + + /** + * Get the default input format. + * + * @return mixed Default input format. + * + * @static + */ + function getDefaultInputFormat() + { + return $GLOBALS['_DATE_SPAN_INPUT_FORMAT']; + } + + // }}} + // {{{ setDefaultFormat() + + /** + * Set the default format. + * + * @param mixed $format New default format. + * + * @return mixed Previous default format. + * + * @static + */ + function setDefaultFormat($format) + { + $old = $GLOBALS['_DATE_SPAN_FORMAT']; + $GLOBALS['_DATE_SPAN_FORMAT'] = $format; + return $old; + } + + // }}} + // {{{ getDefaultFormat() + + /** + * Get the default format. + * + * @return mixed Default format. + * + * @static + */ + function getDefaultFormat() + { + return $GLOBALS['_DATE_SPAN_FORMAT']; + } + + // }}} + // {{{ __clone() + + /** + * Returns a copy of the object (workarround for PHP5 forward compatibility). + * + * @return object Date_Span Copy of the object. + */ + function __clone() { + $c = get_class($this); + $s = new $c; + $s->day = $this->day; + $s->hour = $this->hour; + $s->minute = $this->minute; + $s->second = $this->second; + return $s; + } + + // }}} +} + +// }}} + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ +?> \ No newline at end of file diff --git a/Date/TimeZone.php b/Date/TimeZone.php new file mode 100644 index 0000000..78c9d3f --- /dev/null +++ b/Date/TimeZone.php @@ -0,0 +1,4731 @@ + + * @author Pierre-Alain Joye + * @copyright 1997-2006 Baba Buehler, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: TimeZone.php,v 1.14 2006/11/22 01:03:12 firman Exp $ + * @link http://pear.php.net/package/Date + */ + +// }}} +// {{{ Class: Date_TimeZone + +/** + * TimeZone representation class, along with time zone information data + * + * The default timezone is set from the first valid timezone id found + * in one of the following places, in this order: + * + global $_DATE_TIMEZONE_DEFAULT + * + system environment variable PHP_TZ + * + system environment variable TZ + * + the result of date('T') + * + * If no valid timezone id is found, the default timezone is set to 'UTC'. + * You may also manually set the default timezone by passing a valid id to + * Date_TimeZone::setDefault(). + * + * This class includes time zone data (from zoneinfo) in the form of a + * global array, $_DATE_TIMEZONE_DATA. + * + * @author Baba Buehler + * @copyright 1997-2006 Baba Buehler, Pierre-Alain Joye + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.4.7 + * @link http://pear.php.net/package/Date + */ +class Date_TimeZone +{ + // {{{ Properties + + /** + * Time Zone ID of this time zone + * @var string + */ + var $id; + + /** + * Long Name of this time zone (ie Central Standard Time) + * @var string + */ + var $longname; + + /** + * Short Name of this time zone (ie CST) + * @var string + */ + var $shortname; + + /** + * true if this time zone observes daylight savings time + * @var boolean + */ + var $hasdst; + + /** + * DST Long Name of this time zone + * @var string + */ + var $dstlongname; + + /** + * DST Short Name of this timezone + * @var string + */ + var $dstshortname; + + /** + * offset, in milliseconds, of this timezone + * @var int + */ + var $offset; + + /** + * System Default Time Zone + * @var object Date_TimeZone + */ + var $default; + + // }}} + // {{{ Constructor + + /** + * Constructor + * + * Creates a new Date::TimeZone object, representing the time zone + * specified in $id. If the supplied ID is invalid, the created + * time zone is UTC. + * + * @access public + * @param string $id the time zone id + * @return object Date_TimeZone the new Date_TimeZone object + */ + function Date_TimeZone($id) + { + $_DATE_TIMEZONE_DATA =& $GLOBALS['_DATE_TIMEZONE_DATA']; + if(Date_TimeZone::isValidID($id)) { + $this->id = $id; + $this->longname = $_DATE_TIMEZONE_DATA[$id]['longname']; + $this->shortname = $_DATE_TIMEZONE_DATA[$id]['shortname']; + $this->offset = $_DATE_TIMEZONE_DATA[$id]['offset']; + if($_DATE_TIMEZONE_DATA[$id]['hasdst']) { + $this->hasdst = true; + $this->dstlongname = $_DATE_TIMEZONE_DATA[$id]['dstlongname']; + $this->dstshortname = $_DATE_TIMEZONE_DATA[$id]['dstshortname']; + } else { + $this->hasdst = false; + $this->dstlongname = $this->longname; + $this->dstshortname = $this->shortname; + } + } else { + $this->id = 'UTC'; + $this->longname = $_DATE_TIMEZONE_DATA[$this->id]['longname']; + $this->shortname = $_DATE_TIMEZONE_DATA[$this->id]['shortname']; + $this->hasdst = $_DATE_TIMEZONE_DATA[$this->id]['hasdst']; + $this->offset = $_DATE_TIMEZONE_DATA[$this->id]['offset']; + } + } + + // }}} + // {{{ getDefault() + + /** + * Return a TimeZone object representing the system default time zone + * + * Return a TimeZone object representing the system default time zone, + * which is initialized during the loading of TimeZone.php. + * + * @access public + * @return object Date_TimeZone the default time zone + */ + function getDefault() + { + return new Date_TimeZone($GLOBALS['_DATE_TIMEZONE_DEFAULT']); + } + + // }}} + // {{{ setDefault() + + /** + * Sets the system default time zone to the time zone in $id + * + * Sets the system default time zone to the time zone in $id + * + * @access public + * @param string $id the time zone id to use + */ + function setDefault($id) + { + if(Date_TimeZone::isValidID($id)) { + $GLOBALS['_DATE_TIMEZONE_DEFAULT'] = $id; + } + } + + // }}} + // {{{ isValidID() + + /** + * Tests if given id is represented in the $_DATE_TIMEZONE_DATA time zone data + * + * Tests if given id is represented in the $_DATE_TIMEZONE_DATA time zone data + * + * @access public + * @param string $id the id to test + * @return boolean true if the supplied ID is valid + */ + function isValidID($id) + { + if(isset($GLOBALS['_DATE_TIMEZONE_DATA'][$id])) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ isEqual() + + /** + * Is this time zone equal to another + * + * Tests to see if this time zone is equal (ids match) + * to a given Date_TimeZone object. + * + * @access public + * @param object Date_TimeZone $tz the timezone to test + * @return boolean true if this time zone is equal to the supplied time zone + */ + function isEqual($tz) + { + if(strcasecmp($this->id, $tz->id) == 0) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ isEquivalent() + + /** + * Is this time zone equivalent to another + * + * Tests to see if this time zone is equivalent to + * a given time zone object. Equivalence in this context + * is defined by the two time zones having an equal raw + * offset and an equal setting of "hasdst". This is not true + * equivalence, as the two time zones may have different rules + * for the observance of DST, but this implementation does not + * know DST rules. + * + * @access public + * @param object Date_TimeZone $tz the timezone object to test + * @return boolean true if this time zone is equivalent to the supplied time zone + */ + function isEquivalent($tz) + { + if($this->offset == $tz->offset && $this->hasdst == $tz->hasdst) { + return true; + } else { + return false; + } + } + + // }}} + // {{{ hasDaylightTime() + + /** + * Returns true if this zone observes daylight savings time + * + * Returns true if this zone observes daylight savings time + * + * @access public + * @return boolean true if this time zone has DST + */ + function hasDaylightTime() + { + return $this->hasdst; + } + + // }}} + // {{{ inDaylightTime() + + /** + * Is the given date/time in DST for this time zone + * + * Attempts to determine if a given Date object represents a date/time + * that is in DST for this time zone. WARNINGS: this basically attempts to + * "trick" the system into telling us if we're in DST for a given time zone. + * This uses putenv() which may not work in safe mode, and relies on unix time + * which is only valid for dates from 1970 to ~2038. This relies on the + * underlying OS calls, so it may not work on Windows or on a system where + * zoneinfo is not installed or configured properly. + * + * @access public + * @param object Date $date the date/time to test + * @return boolean true if this date is in DST for this time zone + */ + function inDaylightTime($date) + { + $env_tz = ''; + if(isset($_ENV['TZ']) && getenv('TZ')) { + $env_tz = getenv('TZ'); + } + + putenv('TZ=' . $this->id); + $ltime = localtime($date->getTime(), true); + if ($env_tz != '') { + putenv('TZ=' . $env_tz); + } + return $ltime['tm_isdst']; + } + + // }}} + // {{{ getDSTSavings() + + /** + * Get the DST offset for this time zone + * + * Returns the DST offset of this time zone, in milliseconds, + * if the zone observes DST, zero otherwise. Currently the + * DST offset is hard-coded to one hour. + * + * @access public + * @return int the DST offset, in milliseconds or zero if the zone does not observe DST + */ + function getDSTSavings() + { + if($this->hasdst) { + return 3600000; + } else { + return 0; + } + } + + // }}} + // {{{ getOffset() + + /** + * Get the DST-corrected offset to UTC for the given date + * + * Attempts to get the offset to UTC for a given date/time, taking into + * account daylight savings time, if the time zone observes it and if + * it is in effect. Please see the WARNINGS on Date::TimeZone::inDaylightTime(). + * + * + * @access public + * @param object Date $date the Date to test + * @return int the corrected offset to UTC in milliseconds + */ + function getOffset($date) + { + if($this->inDaylightTime($date)) { + return $this->offset + $this->getDSTSavings(); + } else { + return $this->offset; + } + } + + // }}} + // {{{ getAvailableIDs() + + /** + * Returns the list of valid time zone id strings + * + * Returns the list of valid time zone id strings + * + * @access public + * @return mixed an array of strings with the valid time zone IDs + */ + function getAvailableIDs() + { + return array_keys($GLOBALS['_DATE_TIMEZONE_DATA']); + } + + // }}} + // {{{ getID() + + /** + * Returns the id for this time zone + * + * Returns the time zone id for this time zone, i.e. "America/Chicago" + * + * @access public + * @return string the id + */ + function getID() + { + return $this->id; + } + + // }}} + // {{{ getLongName() + + /** + * Returns the long name for this time zone + * + * Returns the long name for this time zone, + * i.e. "Central Standard Time" + * + * @access public + * @return string the long name + */ + function getLongName() + { + return $this->longname; + } + + // }}} + // {{{ getShortName() + + /** + * Returns the short name for this time zone + * + * Returns the short name for this time zone, i.e. "CST" + * + * @access public + * @return string the short name + */ + function getShortName() + { + return $this->shortname; + } + + // }}} + // {{{ getDSTLongName() + + /** + * Returns the DST long name for this time zone + * + * Returns the DST long name for this time zone, i.e. "Central Daylight Time" + * + * @access public + * @return string the daylight savings time long name + */ + function getDSTLongName() + { + return $this->dstlongname; + } + + // }}} + // {{{ getDSTShortName() + + /** + * Returns the DST short name for this time zone + * + * Returns the DST short name for this time zone, i.e. "CDT" + * + * @access public + * @return string the daylight savings time short name + */ + function getDSTShortName() + { + return $this->dstshortname; + } + + // }}} + // {{{ getRawOffset() + + /** + * Returns the raw (non-DST-corrected) offset from UTC/GMT for this time zone + * + * Returns the raw (non-DST-corrected) offset from UTC/GMT for this time zone + * + * @access public + * @return int the offset, in milliseconds + */ + function getRawOffset() + { + return $this->offset; + } + + // }}} +} + +// }}} + +/** + * Time Zone Data offset is in miliseconds + * + * @global array $GLOBALS['_DATE_TIMEZONE_DATA'] + */ +$GLOBALS['_DATE_TIMEZONE_DATA'] = array( + 'Etc/GMT+12' => array( + 'offset' => -43200000, + 'longname' => 'GMT-12:00', + 'shortname' => 'GMT-12:00', + 'hasdst' => false ), + 'Etc/GMT+11' => array( + 'offset' => -39600000, + 'longname' => 'GMT-11:00', + 'shortname' => 'GMT-11:00', + 'hasdst' => false ), + 'MIT' => array( + 'offset' => -39600000, + 'longname' => 'West Samoa Time', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Pacific/Apia' => array( + 'offset' => -39600000, + 'longname' => 'West Samoa Time', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Pacific/Midway' => array( + 'offset' => -39600000, + 'longname' => 'Samoa Standard Time', + 'shortname' => 'SST', + 'hasdst' => false ), + 'Pacific/Niue' => array( + 'offset' => -39600000, + 'longname' => 'Niue Time', + 'shortname' => 'NUT', + 'hasdst' => false ), + 'Pacific/Pago_Pago' => array( + 'offset' => -39600000, + 'longname' => 'Samoa Standard Time', + 'shortname' => 'SST', + 'hasdst' => false ), + 'Pacific/Samoa' => array( + 'offset' => -39600000, + 'longname' => 'Samoa Standard Time', + 'shortname' => 'SST', + 'hasdst' => false ), + 'US/Samoa' => array( + 'offset' => -39600000, + 'longname' => 'Samoa Standard Time', + 'shortname' => 'SST', + 'hasdst' => false ), + 'America/Adak' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii-Aleutian Standard Time', + 'shortname' => 'HAST', + 'hasdst' => true, + 'dstlongname' => 'Hawaii-Aleutian Daylight Time', + 'dstshortname' => 'HADT' ), + 'America/Atka' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii-Aleutian Standard Time', + 'shortname' => 'HAST', + 'hasdst' => true, + 'dstlongname' => 'Hawaii-Aleutian Daylight Time', + 'dstshortname' => 'HADT' ), + 'Etc/GMT+10' => array( + 'offset' => -36000000, + 'longname' => 'GMT-10:00', + 'shortname' => 'GMT-10:00', + 'hasdst' => false ), + 'HST' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'Pacific/Fakaofo' => array( + 'offset' => -36000000, + 'longname' => 'Tokelau Time', + 'shortname' => 'TKT', + 'hasdst' => false ), + 'Pacific/Honolulu' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'Pacific/Johnston' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'Pacific/Rarotonga' => array( + 'offset' => -36000000, + 'longname' => 'Cook Is. Time', + 'shortname' => 'CKT', + 'hasdst' => false ), + 'Pacific/Tahiti' => array( + 'offset' => -36000000, + 'longname' => 'Tahiti Time', + 'shortname' => 'TAHT', + 'hasdst' => false ), + 'SystemV/HST10' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'US/Aleutian' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii-Aleutian Standard Time', + 'shortname' => 'HAST', + 'hasdst' => true, + 'dstlongname' => 'Hawaii-Aleutian Daylight Time', + 'dstshortname' => 'HADT' ), + 'US/Hawaii' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'Pacific/Marquesas' => array( + 'offset' => -34200000, + 'longname' => 'Marquesas Time', + 'shortname' => 'MART', + 'hasdst' => false ), + 'AST' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'America/Anchorage' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'America/Juneau' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'America/Nome' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'America/Yakutat' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'Etc/GMT+9' => array( + 'offset' => -32400000, + 'longname' => 'GMT-09:00', + 'shortname' => 'GMT-09:00', + 'hasdst' => false ), + 'Pacific/Gambier' => array( + 'offset' => -32400000, + 'longname' => 'Gambier Time', + 'shortname' => 'GAMT', + 'hasdst' => false ), + 'SystemV/YST9' => array( + 'offset' => -32400000, + 'longname' => 'Gambier Time', + 'shortname' => 'GAMT', + 'hasdst' => false ), + 'SystemV/YST9YDT' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'US/Alaska' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'America/Dawson' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Ensenada' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Los_Angeles' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Tijuana' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Vancouver' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Whitehorse' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'Canada/Pacific' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'Canada/Yukon' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'Etc/GMT+8' => array( + 'offset' => -28800000, + 'longname' => 'GMT-08:00', + 'shortname' => 'GMT-08:00', + 'hasdst' => false ), + 'Mexico/BajaNorte' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'PST' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'PST8PDT' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'Pacific/Pitcairn' => array( + 'offset' => -28800000, + 'longname' => 'Pitcairn Standard Time', + 'shortname' => 'PST', + 'hasdst' => false ), + 'SystemV/PST8' => array( + 'offset' => -28800000, + 'longname' => 'Pitcairn Standard Time', + 'shortname' => 'PST', + 'hasdst' => false ), + 'SystemV/PST8PDT' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'US/Pacific' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'US/Pacific-New' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'America/Boise' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Cambridge_Bay' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Chihuahua' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Dawson_Creek' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'America/Denver' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Edmonton' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Hermosillo' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'America/Inuvik' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Mazatlan' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Phoenix' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'America/Shiprock' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Yellowknife' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'Canada/Mountain' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'Etc/GMT+7' => array( + 'offset' => -25200000, + 'longname' => 'GMT-07:00', + 'shortname' => 'GMT-07:00', + 'hasdst' => false ), + 'MST' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'MST7MDT' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'Mexico/BajaSur' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'Navajo' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'PNT' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'SystemV/MST7' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'SystemV/MST7MDT' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'US/Arizona' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => false ), + 'US/Mountain' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'America/Belize' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Cancun' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Chicago' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Costa_Rica' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/El_Salvador' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Guatemala' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Managua' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Menominee' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Merida' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Mexico_City' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Monterrey' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/North_Dakota/Center' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Rainy_River' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Rankin_Inlet' => array( + 'offset' => -21600000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Regina' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Swift_Current' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Tegucigalpa' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'America/Winnipeg' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'CST' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'CST6CDT' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'Canada/Central' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'Canada/East-Saskatchewan' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Canada/Saskatchewan' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Chile/EasterIsland' => array( + 'offset' => -21600000, + 'longname' => 'Easter Is. Time', + 'shortname' => 'EAST', + 'hasdst' => true, + 'dstlongname' => 'Easter Is. Summer Time', + 'dstshortname' => 'EASST' ), + 'Etc/GMT+6' => array( + 'offset' => -21600000, + 'longname' => 'GMT-06:00', + 'shortname' => 'GMT-06:00', + 'hasdst' => false ), + 'Mexico/General' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Pacific/Easter' => array( + 'offset' => -21600000, + 'longname' => 'Easter Is. Time', + 'shortname' => 'EAST', + 'hasdst' => true, + 'dstlongname' => 'Easter Is. Summer Time', + 'dstshortname' => 'EASST' ), + 'Pacific/Galapagos' => array( + 'offset' => -21600000, + 'longname' => 'Galapagos Time', + 'shortname' => 'GALT', + 'hasdst' => false ), + 'SystemV/CST6' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'SystemV/CST6CDT' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'US/Central' => array( + 'offset' => -21600000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Bogota' => array( + 'offset' => -18000000, + 'longname' => 'Colombia Time', + 'shortname' => 'COT', + 'hasdst' => false ), + 'America/Cayman' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Detroit' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Eirunepe' => array( + 'offset' => -18000000, + 'longname' => 'Acre Time', + 'shortname' => 'ACT', + 'hasdst' => false ), + 'America/Fort_Wayne' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Grand_Turk' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Guayaquil' => array( + 'offset' => -18000000, + 'longname' => 'Ecuador Time', + 'shortname' => 'ECT', + 'hasdst' => false ), + 'America/Havana' => array( + 'offset' => -18000000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'America/Indiana/Indianapolis' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Indiana/Knox' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Indiana/Marengo' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Indiana/Vevay' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Indianapolis' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Iqaluit' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Jamaica' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Kentucky/Louisville' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Kentucky/Monticello' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Knox_IN' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Lima' => array( + 'offset' => -18000000, + 'longname' => 'Peru Time', + 'shortname' => 'PET', + 'hasdst' => false ), + 'America/Louisville' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Montreal' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Nassau' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/New_York' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Nipigon' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Panama' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Pangnirtung' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Port-au-Prince' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'America/Porto_Acre' => array( + 'offset' => -18000000, + 'longname' => 'Acre Time', + 'shortname' => 'ACT', + 'hasdst' => false ), + 'America/Rio_Branco' => array( + 'offset' => -18000000, + 'longname' => 'Acre Time', + 'shortname' => 'ACT', + 'hasdst' => false ), + 'America/Thunder_Bay' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'Brazil/Acre' => array( + 'offset' => -18000000, + 'longname' => 'Acre Time', + 'shortname' => 'ACT', + 'hasdst' => false ), + 'Canada/Eastern' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'Cuba' => array( + 'offset' => -18000000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'EST' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'EST5EDT' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'Etc/GMT+5' => array( + 'offset' => -18000000, + 'longname' => 'GMT-05:00', + 'shortname' => 'GMT-05:00', + 'hasdst' => false ), + 'IET' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'Jamaica' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'SystemV/EST5' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'SystemV/EST5EDT' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'US/East-Indiana' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'US/Eastern' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'US/Indiana-Starke' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => false ), + 'US/Michigan' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'America/Anguilla' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Antigua' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Aruba' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Asuncion' => array( + 'offset' => -14400000, + 'longname' => 'Paraguay Time', + 'shortname' => 'PYT', + 'hasdst' => true, + 'dstlongname' => 'Paraguay Summer Time', + 'dstshortname' => 'PYST' ), + 'America/Barbados' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Boa_Vista' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => false ), + 'America/Caracas' => array( + 'offset' => -14400000, + 'longname' => 'Venezuela Time', + 'shortname' => 'VET', + 'hasdst' => false ), + 'America/Cuiaba' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => true, + 'dstlongname' => 'Amazon Summer Time', + 'dstshortname' => 'AMST' ), + 'America/Curacao' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Dominica' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Glace_Bay' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'America/Goose_Bay' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'America/Grenada' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Guadeloupe' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Guyana' => array( + 'offset' => -14400000, + 'longname' => 'Guyana Time', + 'shortname' => 'GYT', + 'hasdst' => false ), + 'America/Halifax' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'America/La_Paz' => array( + 'offset' => -14400000, + 'longname' => 'Bolivia Time', + 'shortname' => 'BOT', + 'hasdst' => false ), + 'America/Manaus' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => false ), + 'America/Martinique' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Montserrat' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Port_of_Spain' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Porto_Velho' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => false ), + 'America/Puerto_Rico' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Santiago' => array( + 'offset' => -14400000, + 'longname' => 'Chile Time', + 'shortname' => 'CLT', + 'hasdst' => true, + 'dstlongname' => 'Chile Summer Time', + 'dstshortname' => 'CLST' ), + 'America/Santo_Domingo' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/St_Kitts' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/St_Lucia' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/St_Thomas' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/St_Vincent' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Thule' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Tortola' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'America/Virgin' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'Antarctica/Palmer' => array( + 'offset' => -14400000, + 'longname' => 'Chile Time', + 'shortname' => 'CLT', + 'hasdst' => true, + 'dstlongname' => 'Chile Summer Time', + 'dstshortname' => 'CLST' ), + 'Atlantic/Bermuda' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'Atlantic/Stanley' => array( + 'offset' => -14400000, + 'longname' => 'Falkland Is. Time', + 'shortname' => 'FKT', + 'hasdst' => true, + 'dstlongname' => 'Falkland Is. Summer Time', + 'dstshortname' => 'FKST' ), + 'Brazil/West' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => false ), + 'Canada/Atlantic' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'Chile/Continental' => array( + 'offset' => -14400000, + 'longname' => 'Chile Time', + 'shortname' => 'CLT', + 'hasdst' => true, + 'dstlongname' => 'Chile Summer Time', + 'dstshortname' => 'CLST' ), + 'Etc/GMT+4' => array( + 'offset' => -14400000, + 'longname' => 'GMT-04:00', + 'shortname' => 'GMT-04:00', + 'hasdst' => false ), + 'PRT' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'SystemV/AST4' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'SystemV/AST4ADT' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'America/St_Johns' => array( + 'offset' => -12600000, + 'longname' => 'Newfoundland Standard Time', + 'shortname' => 'NST', + 'hasdst' => true, + 'dstlongname' => 'Newfoundland Daylight Time', + 'dstshortname' => 'NDT' ), + 'CNT' => array( + 'offset' => -12600000, + 'longname' => 'Newfoundland Standard Time', + 'shortname' => 'NST', + 'hasdst' => true, + 'dstlongname' => 'Newfoundland Daylight Time', + 'dstshortname' => 'NDT' ), + 'Canada/Newfoundland' => array( + 'offset' => -12600000, + 'longname' => 'Newfoundland Standard Time', + 'shortname' => 'NST', + 'hasdst' => true, + 'dstlongname' => 'Newfoundland Daylight Time', + 'dstshortname' => 'NDT' ), + 'AGT' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Araguaina' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'America/Belem' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => false ), + 'America/Buenos_Aires' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Catamarca' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Cayenne' => array( + 'offset' => -10800000, + 'longname' => 'French Guiana Time', + 'shortname' => 'GFT', + 'hasdst' => false ), + 'America/Cordoba' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Fortaleza' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'America/Godthab' => array( + 'offset' => -10800000, + 'longname' => 'Western Greenland Time', + 'shortname' => 'WGT', + 'hasdst' => true, + 'dstlongname' => 'Western Greenland Summer Time', + 'dstshortname' => 'WGST' ), + 'America/Jujuy' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Maceio' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'America/Mendoza' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Miquelon' => array( + 'offset' => -10800000, + 'longname' => 'Pierre & Miquelon Standard Time', + 'shortname' => 'PMST', + 'hasdst' => true, + 'dstlongname' => 'Pierre & Miquelon Daylight Time', + 'dstshortname' => 'PMDT' ), + 'America/Montevideo' => array( + 'offset' => -10800000, + 'longname' => 'Uruguay Time', + 'shortname' => 'UYT', + 'hasdst' => false ), + 'America/Paramaribo' => array( + 'offset' => -10800000, + 'longname' => 'Suriname Time', + 'shortname' => 'SRT', + 'hasdst' => false ), + 'America/Recife' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'America/Rosario' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'America/Sao_Paulo' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'BET' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'Brazil/East' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'Etc/GMT+3' => array( + 'offset' => -10800000, + 'longname' => 'GMT-03:00', + 'shortname' => 'GMT-03:00', + 'hasdst' => false ), + 'America/Noronha' => array( + 'offset' => -7200000, + 'longname' => 'Fernando de Noronha Time', + 'shortname' => 'FNT', + 'hasdst' => false ), + 'Atlantic/South_Georgia' => array( + 'offset' => -7200000, + 'longname' => 'South Georgia Standard Time', + 'shortname' => 'GST', + 'hasdst' => false ), + 'Brazil/DeNoronha' => array( + 'offset' => -7200000, + 'longname' => 'Fernando de Noronha Time', + 'shortname' => 'FNT', + 'hasdst' => false ), + 'Etc/GMT+2' => array( + 'offset' => -7200000, + 'longname' => 'GMT-02:00', + 'shortname' => 'GMT-02:00', + 'hasdst' => false ), + 'America/Scoresbysund' => array( + 'offset' => -3600000, + 'longname' => 'Eastern Greenland Time', + 'shortname' => 'EGT', + 'hasdst' => true, + 'dstlongname' => 'Eastern Greenland Summer Time', + 'dstshortname' => 'EGST' ), + 'Atlantic/Azores' => array( + 'offset' => -3600000, + 'longname' => 'Azores Time', + 'shortname' => 'AZOT', + 'hasdst' => true, + 'dstlongname' => 'Azores Summer Time', + 'dstshortname' => 'AZOST' ), + 'Atlantic/Cape_Verde' => array( + 'offset' => -3600000, + 'longname' => 'Cape Verde Time', + 'shortname' => 'CVT', + 'hasdst' => false ), + 'Etc/GMT+1' => array( + 'offset' => -3600000, + 'longname' => 'GMT-01:00', + 'shortname' => 'GMT-01:00', + 'hasdst' => false ), + 'Africa/Abidjan' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Accra' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Bamako' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Banjul' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Bissau' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Casablanca' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => false ), + 'Africa/Conakry' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Dakar' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/El_Aaiun' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => false ), + 'Africa/Freetown' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Lome' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Monrovia' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Nouakchott' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Ouagadougou' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Sao_Tome' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Africa/Timbuktu' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'America/Danmarkshavn' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Atlantic/Canary' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'Atlantic/Faeroe' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'Atlantic/Madeira' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'Atlantic/Reykjavik' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Atlantic/St_Helena' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Eire' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'Irish Summer Time', + 'dstshortname' => 'IST' ), + 'Etc/GMT' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Etc/GMT+0' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Etc/GMT-0' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Etc/GMT0' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Etc/Greenwich' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Etc/UCT' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Etc/UTC' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Etc/Universal' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Etc/Zulu' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Europe/Belfast' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'British Summer Time', + 'dstshortname' => 'BST' ), + 'Europe/Dublin' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'Irish Summer Time', + 'dstshortname' => 'IST' ), + 'Europe/Lisbon' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'Europe/London' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'British Summer Time', + 'dstshortname' => 'BST' ), + 'GB' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'British Summer Time', + 'dstshortname' => 'BST' ), + 'GB-Eire' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => true, + 'dstlongname' => 'British Summer Time', + 'dstshortname' => 'BST' ), + 'GMT' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'GMT0' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Greenwich' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Iceland' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Portugal' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'UCT' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'UTC' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Universal' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'WET' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'Zulu' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Africa/Algiers' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => false ), + 'Africa/Bangui' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Brazzaville' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Ceuta' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Africa/Douala' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Kinshasa' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Lagos' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Libreville' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Luanda' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Malabo' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Ndjamena' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Niamey' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Porto-Novo' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => false ), + 'Africa/Tunis' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => false ), + 'Africa/Windhoek' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => true, + 'dstlongname' => 'Western African Summer Time', + 'dstshortname' => 'WAST' ), + 'Arctic/Longyearbyen' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Atlantic/Jan_Mayen' => array( + 'offset' => 3600000, + 'longname' => 'Eastern Greenland Time', + 'shortname' => 'EGT', + 'hasdst' => true, + 'dstlongname' => 'Eastern Greenland Summer Time', + 'dstshortname' => 'EGST' ), + 'CET' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'CEST' => array( + 'offset' => 3600000, + 'longname' => "Central European Time", + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => "Central European Summer Time", + 'dstshortname' => 'CEST' ), + 'ECT' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Etc/GMT-1' => array( + 'offset' => 3600000, + 'longname' => 'GMT+01:00', + 'shortname' => 'GMT+01:00', + 'hasdst' => false ), + 'Europe/Amsterdam' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Andorra' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Belgrade' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Berlin' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Bratislava' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Brussels' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Budapest' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Copenhagen' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Gibraltar' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Ljubljana' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Luxembourg' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Madrid' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Malta' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Monaco' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Oslo' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Paris' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Prague' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Rome' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/San_Marino' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Sarajevo' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Skopje' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Stockholm' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Tirane' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Vaduz' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Vatican' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Vienna' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Warsaw' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Zagreb' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Europe/Zurich' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'MET' => array( + 'offset' => 3600000, + 'longname' => 'Middle Europe Time', + 'shortname' => 'MET', + 'hasdst' => true, + 'dstlongname' => 'Middle Europe Summer Time', + 'dstshortname' => 'MEST' ), + 'Poland' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'ART' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Africa/Blantyre' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Bujumbura' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Cairo' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Africa/Gaborone' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Harare' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Johannesburg' => array( + 'offset' => 7200000, + 'longname' => 'South Africa Standard Time', + 'shortname' => 'SAST', + 'hasdst' => false ), + 'Africa/Kigali' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Lubumbashi' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Lusaka' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Maputo' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'Africa/Maseru' => array( + 'offset' => 7200000, + 'longname' => 'South Africa Standard Time', + 'shortname' => 'SAST', + 'hasdst' => false ), + 'Africa/Mbabane' => array( + 'offset' => 7200000, + 'longname' => 'South Africa Standard Time', + 'shortname' => 'SAST', + 'hasdst' => false ), + 'Africa/Tripoli' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => false ), + 'Asia/Amman' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Beirut' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Damascus' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Gaza' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Istanbul' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Jerusalem' => array( + 'offset' => 7200000, + 'longname' => 'Israel Standard Time', + 'shortname' => 'IST', + 'hasdst' => true, + 'dstlongname' => 'Israel Daylight Time', + 'dstshortname' => 'IDT' ), + 'Asia/Nicosia' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Asia/Tel_Aviv' => array( + 'offset' => 7200000, + 'longname' => 'Israel Standard Time', + 'shortname' => 'IST', + 'hasdst' => true, + 'dstlongname' => 'Israel Daylight Time', + 'dstshortname' => 'IDT' ), + 'CAT' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'EET' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Egypt' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Etc/GMT-2' => array( + 'offset' => 7200000, + 'longname' => 'GMT+02:00', + 'shortname' => 'GMT+02:00', + 'hasdst' => false ), + 'Europe/Athens' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Bucharest' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Chisinau' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Helsinki' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Istanbul' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Kaliningrad' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Kiev' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Minsk' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Nicosia' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Riga' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Simferopol' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Sofia' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Tallinn' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => false ), + 'Europe/Tiraspol' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Uzhgorod' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Europe/Vilnius' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => false ), + 'Europe/Zaporozhye' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Israel' => array( + 'offset' => 7200000, + 'longname' => 'Israel Standard Time', + 'shortname' => 'IST', + 'hasdst' => true, + 'dstlongname' => 'Israel Daylight Time', + 'dstshortname' => 'IDT' ), + 'Libya' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => false ), + 'Turkey' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Africa/Addis_Ababa' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Asmera' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Dar_es_Salaam' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Djibouti' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Kampala' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Khartoum' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Mogadishu' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Africa/Nairobi' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Antarctica/Syowa' => array( + 'offset' => 10800000, + 'longname' => 'Syowa Time', + 'shortname' => 'SYOT', + 'hasdst' => false ), + 'Asia/Aden' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'Asia/Baghdad' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Arabia Daylight Time', + 'dstshortname' => 'ADT' ), + 'Asia/Bahrain' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'Asia/Kuwait' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'Asia/Qatar' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'Asia/Riyadh' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'EAT' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Etc/GMT-3' => array( + 'offset' => 10800000, + 'longname' => 'GMT+03:00', + 'shortname' => 'GMT+03:00', + 'hasdst' => false ), + 'Europe/Moscow' => array( + 'offset' => 10800000, + 'longname' => 'Moscow Standard Time', + 'shortname' => 'MSK', + 'hasdst' => true, + 'dstlongname' => 'Moscow Daylight Time', + 'dstshortname' => 'MSD' ), + 'Indian/Antananarivo' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Indian/Comoro' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Indian/Mayotte' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'W-SU' => array( + 'offset' => 10800000, + 'longname' => 'Moscow Standard Time', + 'shortname' => 'MSK', + 'hasdst' => true, + 'dstlongname' => 'Moscow Daylight Time', + 'dstshortname' => 'MSD' ), + 'Asia/Riyadh87' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Asia/Riyadh88' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Asia/Riyadh89' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Mideast/Riyadh87' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Mideast/Riyadh88' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Mideast/Riyadh89' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Asia/Tehran' => array( + 'offset' => 12600000, + 'longname' => 'Iran Time', + 'shortname' => 'IRT', + 'hasdst' => true, + 'dstlongname' => 'Iran Sumer Time', + 'dstshortname' => 'IRST' ), + 'Iran' => array( + 'offset' => 12600000, + 'longname' => 'Iran Time', + 'shortname' => 'IRT', + 'hasdst' => true, + 'dstlongname' => 'Iran Sumer Time', + 'dstshortname' => 'IRST' ), + 'Asia/Aqtau' => array( + 'offset' => 14400000, + 'longname' => 'Aqtau Time', + 'shortname' => 'AQTT', + 'hasdst' => true, + 'dstlongname' => 'Aqtau Summer Time', + 'dstshortname' => 'AQTST' ), + 'Asia/Baku' => array( + 'offset' => 14400000, + 'longname' => 'Azerbaijan Time', + 'shortname' => 'AZT', + 'hasdst' => true, + 'dstlongname' => 'Azerbaijan Summer Time', + 'dstshortname' => 'AZST' ), + 'Asia/Dubai' => array( + 'offset' => 14400000, + 'longname' => 'Gulf Standard Time', + 'shortname' => 'GST', + 'hasdst' => false ), + 'Asia/Muscat' => array( + 'offset' => 14400000, + 'longname' => 'Gulf Standard Time', + 'shortname' => 'GST', + 'hasdst' => false ), + 'Asia/Tbilisi' => array( + 'offset' => 14400000, + 'longname' => 'Georgia Time', + 'shortname' => 'GET', + 'hasdst' => true, + 'dstlongname' => 'Georgia Summer Time', + 'dstshortname' => 'GEST' ), + 'Asia/Yerevan' => array( + 'offset' => 14400000, + 'longname' => 'Armenia Time', + 'shortname' => 'AMT', + 'hasdst' => true, + 'dstlongname' => 'Armenia Summer Time', + 'dstshortname' => 'AMST' ), + 'Etc/GMT-4' => array( + 'offset' => 14400000, + 'longname' => 'GMT+04:00', + 'shortname' => 'GMT+04:00', + 'hasdst' => false ), + 'Europe/Samara' => array( + 'offset' => 14400000, + 'longname' => 'Samara Time', + 'shortname' => 'SAMT', + 'hasdst' => true, + 'dstlongname' => 'Samara Summer Time', + 'dstshortname' => 'SAMST' ), + 'Indian/Mahe' => array( + 'offset' => 14400000, + 'longname' => 'Seychelles Time', + 'shortname' => 'SCT', + 'hasdst' => false ), + 'Indian/Mauritius' => array( + 'offset' => 14400000, + 'longname' => 'Mauritius Time', + 'shortname' => 'MUT', + 'hasdst' => false ), + 'Indian/Reunion' => array( + 'offset' => 14400000, + 'longname' => 'Reunion Time', + 'shortname' => 'RET', + 'hasdst' => false ), + 'NET' => array( + 'offset' => 14400000, + 'longname' => 'Armenia Time', + 'shortname' => 'AMT', + 'hasdst' => true, + 'dstlongname' => 'Armenia Summer Time', + 'dstshortname' => 'AMST' ), + 'Asia/Kabul' => array( + 'offset' => 16200000, + 'longname' => 'Afghanistan Time', + 'shortname' => 'AFT', + 'hasdst' => false ), + 'Asia/Aqtobe' => array( + 'offset' => 18000000, + 'longname' => 'Aqtobe Time', + 'shortname' => 'AQTT', + 'hasdst' => true, + 'dstlongname' => 'Aqtobe Summer Time', + 'dstshortname' => 'AQTST' ), + 'Asia/Ashgabat' => array( + 'offset' => 18000000, + 'longname' => 'Turkmenistan Time', + 'shortname' => 'TMT', + 'hasdst' => false ), + 'Asia/Ashkhabad' => array( + 'offset' => 18000000, + 'longname' => 'Turkmenistan Time', + 'shortname' => 'TMT', + 'hasdst' => false ), + 'Asia/Bishkek' => array( + 'offset' => 18000000, + 'longname' => 'Kirgizstan Time', + 'shortname' => 'KGT', + 'hasdst' => true, + 'dstlongname' => 'Kirgizstan Summer Time', + 'dstshortname' => 'KGST' ), + 'Asia/Dushanbe' => array( + 'offset' => 18000000, + 'longname' => 'Tajikistan Time', + 'shortname' => 'TJT', + 'hasdst' => false ), + 'Asia/Karachi' => array( + 'offset' => 18000000, + 'longname' => 'Pakistan Time', + 'shortname' => 'PKT', + 'hasdst' => false ), + 'Asia/Samarkand' => array( + 'offset' => 18000000, + 'longname' => 'Turkmenistan Time', + 'shortname' => 'TMT', + 'hasdst' => false ), + 'Asia/Tashkent' => array( + 'offset' => 18000000, + 'longname' => 'Uzbekistan Time', + 'shortname' => 'UZT', + 'hasdst' => false ), + 'Asia/Yekaterinburg' => array( + 'offset' => 18000000, + 'longname' => 'Yekaterinburg Time', + 'shortname' => 'YEKT', + 'hasdst' => true, + 'dstlongname' => 'Yekaterinburg Summer Time', + 'dstshortname' => 'YEKST' ), + 'Etc/GMT-5' => array( + 'offset' => 18000000, + 'longname' => 'GMT+05:00', + 'shortname' => 'GMT+05:00', + 'hasdst' => false ), + 'Indian/Kerguelen' => array( + 'offset' => 18000000, + 'longname' => 'French Southern & Antarctic Lands Time', + 'shortname' => 'TFT', + 'hasdst' => false ), + 'Indian/Maldives' => array( + 'offset' => 18000000, + 'longname' => 'Maldives Time', + 'shortname' => 'MVT', + 'hasdst' => false ), + 'PLT' => array( + 'offset' => 18000000, + 'longname' => 'Pakistan Time', + 'shortname' => 'PKT', + 'hasdst' => false ), + 'Asia/Calcutta' => array( + 'offset' => 19800000, + 'longname' => 'India Standard Time', + 'shortname' => 'IST', + 'hasdst' => false ), + 'IST' => array( + 'offset' => 19800000, + 'longname' => 'India Standard Time', + 'shortname' => 'IST', + 'hasdst' => false ), + 'Asia/Katmandu' => array( + 'offset' => 20700000, + 'longname' => 'Nepal Time', + 'shortname' => 'NPT', + 'hasdst' => false ), + 'Antarctica/Mawson' => array( + 'offset' => 21600000, + 'longname' => 'Mawson Time', + 'shortname' => 'MAWT', + 'hasdst' => false ), + 'Antarctica/Vostok' => array( + 'offset' => 21600000, + 'longname' => 'Vostok time', + 'shortname' => 'VOST', + 'hasdst' => false ), + 'Asia/Almaty' => array( + 'offset' => 21600000, + 'longname' => 'Alma-Ata Time', + 'shortname' => 'ALMT', + 'hasdst' => true, + 'dstlongname' => 'Alma-Ata Summer Time', + 'dstshortname' => 'ALMST' ), + 'Asia/Colombo' => array( + 'offset' => 21600000, + 'longname' => 'Sri Lanka Time', + 'shortname' => 'LKT', + 'hasdst' => false ), + 'Asia/Dacca' => array( + 'offset' => 21600000, + 'longname' => 'Bangladesh Time', + 'shortname' => 'BDT', + 'hasdst' => false ), + 'Asia/Dhaka' => array( + 'offset' => 21600000, + 'longname' => 'Bangladesh Time', + 'shortname' => 'BDT', + 'hasdst' => false ), + 'Asia/Novosibirsk' => array( + 'offset' => 21600000, + 'longname' => 'Novosibirsk Time', + 'shortname' => 'NOVT', + 'hasdst' => true, + 'dstlongname' => 'Novosibirsk Summer Time', + 'dstshortname' => 'NOVST' ), + 'Asia/Omsk' => array( + 'offset' => 21600000, + 'longname' => 'Omsk Time', + 'shortname' => 'OMST', + 'hasdst' => true, + 'dstlongname' => 'Omsk Summer Time', + 'dstshortname' => 'OMSST' ), + 'Asia/Thimbu' => array( + 'offset' => 21600000, + 'longname' => 'Bhutan Time', + 'shortname' => 'BTT', + 'hasdst' => false ), + 'Asia/Thimphu' => array( + 'offset' => 21600000, + 'longname' => 'Bhutan Time', + 'shortname' => 'BTT', + 'hasdst' => false ), + 'BDT' => array( + 'offset' => 21600000, + 'longname' => 'Bangladesh Time', + 'shortname' => 'BDT', + 'hasdst' => false ), + 'Etc/GMT-6' => array( + 'offset' => 21600000, + 'longname' => 'GMT+06:00', + 'shortname' => 'GMT+06:00', + 'hasdst' => false ), + 'Indian/Chagos' => array( + 'offset' => 21600000, + 'longname' => 'Indian Ocean Territory Time', + 'shortname' => 'IOT', + 'hasdst' => false ), + 'Asia/Rangoon' => array( + 'offset' => 23400000, + 'longname' => 'Myanmar Time', + 'shortname' => 'MMT', + 'hasdst' => false ), + 'Indian/Cocos' => array( + 'offset' => 23400000, + 'longname' => 'Cocos Islands Time', + 'shortname' => 'CCT', + 'hasdst' => false ), + 'Antarctica/Davis' => array( + 'offset' => 25200000, + 'longname' => 'Davis Time', + 'shortname' => 'DAVT', + 'hasdst' => false ), + 'Asia/Bangkok' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Asia/Hovd' => array( + 'offset' => 25200000, + 'longname' => 'Hovd Time', + 'shortname' => 'HOVT', + 'hasdst' => false ), + 'Asia/Jakarta' => array( + 'offset' => 25200000, + 'longname' => 'West Indonesia Time', + 'shortname' => 'WIT', + 'hasdst' => false ), + 'Asia/Krasnoyarsk' => array( + 'offset' => 25200000, + 'longname' => 'Krasnoyarsk Time', + 'shortname' => 'KRAT', + 'hasdst' => true, + 'dstlongname' => 'Krasnoyarsk Summer Time', + 'dstshortname' => 'KRAST' ), + 'Asia/Phnom_Penh' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Asia/Pontianak' => array( + 'offset' => 25200000, + 'longname' => 'West Indonesia Time', + 'shortname' => 'WIT', + 'hasdst' => false ), + 'Asia/Saigon' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Asia/Vientiane' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Etc/GMT-7' => array( + 'offset' => 25200000, + 'longname' => 'GMT+07:00', + 'shortname' => 'GMT+07:00', + 'hasdst' => false ), + 'Indian/Christmas' => array( + 'offset' => 25200000, + 'longname' => 'Christmas Island Time', + 'shortname' => 'CXT', + 'hasdst' => false ), + 'VST' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Antarctica/Casey' => array( + 'offset' => 28800000, + 'longname' => 'Western Standard Time (Australia)', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Asia/Brunei' => array( + 'offset' => 28800000, + 'longname' => 'Brunei Time', + 'shortname' => 'BNT', + 'hasdst' => false ), + 'Asia/Chongqing' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Chungking' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Harbin' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Hong_Kong' => array( + 'offset' => 28800000, + 'longname' => 'Hong Kong Time', + 'shortname' => 'HKT', + 'hasdst' => false ), + 'Asia/Irkutsk' => array( + 'offset' => 28800000, + 'longname' => 'Irkutsk Time', + 'shortname' => 'IRKT', + 'hasdst' => true, + 'dstlongname' => 'Irkutsk Summer Time', + 'dstshortname' => 'IRKST' ), + 'Asia/Kashgar' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Kuala_Lumpur' => array( + 'offset' => 28800000, + 'longname' => 'Malaysia Time', + 'shortname' => 'MYT', + 'hasdst' => false ), + 'Asia/Kuching' => array( + 'offset' => 28800000, + 'longname' => 'Malaysia Time', + 'shortname' => 'MYT', + 'hasdst' => false ), + 'Asia/Macao' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Manila' => array( + 'offset' => 28800000, + 'longname' => 'Philippines Time', + 'shortname' => 'PHT', + 'hasdst' => false ), + 'Asia/Shanghai' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Singapore' => array( + 'offset' => 28800000, + 'longname' => 'Singapore Time', + 'shortname' => 'SGT', + 'hasdst' => false ), + 'Asia/Taipei' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Asia/Ujung_Pandang' => array( + 'offset' => 28800000, + 'longname' => 'Central Indonesia Time', + 'shortname' => 'CIT', + 'hasdst' => false ), + 'Asia/Ulaanbaatar' => array( + 'offset' => 28800000, + 'longname' => 'Ulaanbaatar Time', + 'shortname' => 'ULAT', + 'hasdst' => false ), + 'Asia/Ulan_Bator' => array( + 'offset' => 28800000, + 'longname' => 'Ulaanbaatar Time', + 'shortname' => 'ULAT', + 'hasdst' => false ), + 'Asia/Urumqi' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Australia/Perth' => array( + 'offset' => 28800000, + 'longname' => 'Western Standard Time (Australia)', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Australia/West' => array( + 'offset' => 28800000, + 'longname' => 'Western Standard Time (Australia)', + 'shortname' => 'WST', + 'hasdst' => false ), + 'CTT' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Etc/GMT-8' => array( + 'offset' => 28800000, + 'longname' => 'GMT+08:00', + 'shortname' => 'GMT+08:00', + 'hasdst' => false ), + 'Hongkong' => array( + 'offset' => 28800000, + 'longname' => 'Hong Kong Time', + 'shortname' => 'HKT', + 'hasdst' => false ), + 'PRC' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Singapore' => array( + 'offset' => 28800000, + 'longname' => 'Singapore Time', + 'shortname' => 'SGT', + 'hasdst' => false ), + 'Asia/Choibalsan' => array( + 'offset' => 32400000, + 'longname' => 'Choibalsan Time', + 'shortname' => 'CHOT', + 'hasdst' => false ), + 'Asia/Dili' => array( + 'offset' => 32400000, + 'longname' => 'East Timor Time', + 'shortname' => 'TPT', + 'hasdst' => false ), + 'Asia/Jayapura' => array( + 'offset' => 32400000, + 'longname' => 'East Indonesia Time', + 'shortname' => 'EIT', + 'hasdst' => false ), + 'Asia/Pyongyang' => array( + 'offset' => 32400000, + 'longname' => 'Korea Standard Time', + 'shortname' => 'KST', + 'hasdst' => false ), + 'Asia/Seoul' => array( + 'offset' => 32400000, + 'longname' => 'Korea Standard Time', + 'shortname' => 'KST', + 'hasdst' => false ), + 'Asia/Tokyo' => array( + 'offset' => 32400000, + 'longname' => 'Japan Standard Time', + 'shortname' => 'JST', + 'hasdst' => false ), + 'Asia/Yakutsk' => array( + 'offset' => 32400000, + 'longname' => 'Yakutsk Time', + 'shortname' => 'YAKT', + 'hasdst' => true, + 'dstlongname' => 'Yaktsk Summer Time', + 'dstshortname' => 'YAKST' ), + 'Etc/GMT-9' => array( + 'offset' => 32400000, + 'longname' => 'GMT+09:00', + 'shortname' => 'GMT+09:00', + 'hasdst' => false ), + 'JST' => array( + 'offset' => 32400000, + 'longname' => 'Japan Standard Time', + 'shortname' => 'JST', + 'hasdst' => false ), + 'Japan' => array( + 'offset' => 32400000, + 'longname' => 'Japan Standard Time', + 'shortname' => 'JST', + 'hasdst' => false ), + 'Pacific/Palau' => array( + 'offset' => 32400000, + 'longname' => 'Palau Time', + 'shortname' => 'PWT', + 'hasdst' => false ), + 'ROK' => array( + 'offset' => 32400000, + 'longname' => 'Korea Standard Time', + 'shortname' => 'KST', + 'hasdst' => false ), + 'ACT' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (Northern Territory)', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Australia/Adelaide' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia)', + 'dstshortname' => 'CST' ), + 'Australia/Broken_Hill' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia/New South Wales)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia/New South Wales)', + 'dstshortname' => 'CST' ), + 'Australia/Darwin' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (Northern Territory)', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Australia/North' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (Northern Territory)', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Australia/South' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia)', + 'dstshortname' => 'CST' ), + 'Australia/Yancowinna' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia/New South Wales)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia/New South Wales)', + 'dstshortname' => 'CST' ), + 'AET' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Antarctica/DumontDUrville' => array( + 'offset' => 36000000, + 'longname' => 'Dumont-d\'Urville Time', + 'shortname' => 'DDUT', + 'hasdst' => false ), + 'Asia/Sakhalin' => array( + 'offset' => 36000000, + 'longname' => 'Sakhalin Time', + 'shortname' => 'SAKT', + 'hasdst' => true, + 'dstlongname' => 'Sakhalin Summer Time', + 'dstshortname' => 'SAKST' ), + 'Asia/Vladivostok' => array( + 'offset' => 36000000, + 'longname' => 'Vladivostok Time', + 'shortname' => 'VLAT', + 'hasdst' => true, + 'dstlongname' => 'Vladivostok Summer Time', + 'dstshortname' => 'VLAST' ), + 'Australia/ACT' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Australia/Brisbane' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Queensland)', + 'shortname' => 'EST', + 'hasdst' => false ), + 'Australia/Canberra' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Australia/Hobart' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Tasmania)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Tasmania)', + 'dstshortname' => 'EST' ), + 'Australia/Lindeman' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Queensland)', + 'shortname' => 'EST', + 'hasdst' => false ), + 'Australia/Melbourne' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Victoria)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Victoria)', + 'dstshortname' => 'EST' ), + 'Australia/NSW' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Australia/Queensland' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Queensland)', + 'shortname' => 'EST', + 'hasdst' => false ), + 'Australia/Sydney' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Australia/Tasmania' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Tasmania)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Tasmania)', + 'dstshortname' => 'EST' ), + 'Australia/Victoria' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Victoria)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Victoria)', + 'dstshortname' => 'EST' ), + 'Etc/GMT-10' => array( + 'offset' => 36000000, + 'longname' => 'GMT+10:00', + 'shortname' => 'GMT+10:00', + 'hasdst' => false ), + 'Pacific/Guam' => array( + 'offset' => 36000000, + 'longname' => 'Chamorro Standard Time', + 'shortname' => 'ChST', + 'hasdst' => false ), + 'Pacific/Port_Moresby' => array( + 'offset' => 36000000, + 'longname' => 'Papua New Guinea Time', + 'shortname' => 'PGT', + 'hasdst' => false ), + 'Pacific/Saipan' => array( + 'offset' => 36000000, + 'longname' => 'Chamorro Standard Time', + 'shortname' => 'ChST', + 'hasdst' => false ), + 'Pacific/Truk' => array( + 'offset' => 36000000, + 'longname' => 'Truk Time', + 'shortname' => 'TRUT', + 'hasdst' => false ), + 'Pacific/Yap' => array( + 'offset' => 36000000, + 'longname' => 'Yap Time', + 'shortname' => 'YAPT', + 'hasdst' => false ), + 'Australia/LHI' => array( + 'offset' => 37800000, + 'longname' => 'Load Howe Standard Time', + 'shortname' => 'LHST', + 'hasdst' => true, + 'dstlongname' => 'Load Howe Summer Time', + 'dstshortname' => 'LHST' ), + 'Australia/Lord_Howe' => array( + 'offset' => 37800000, + 'longname' => 'Load Howe Standard Time', + 'shortname' => 'LHST', + 'hasdst' => true, + 'dstlongname' => 'Load Howe Summer Time', + 'dstshortname' => 'LHST' ), + 'Asia/Magadan' => array( + 'offset' => 39600000, + 'longname' => 'Magadan Time', + 'shortname' => 'MAGT', + 'hasdst' => true, + 'dstlongname' => 'Magadan Summer Time', + 'dstshortname' => 'MAGST' ), + 'Etc/GMT-11' => array( + 'offset' => 39600000, + 'longname' => 'GMT+11:00', + 'shortname' => 'GMT+11:00', + 'hasdst' => false ), + 'Pacific/Efate' => array( + 'offset' => 39600000, + 'longname' => 'Vanuatu Time', + 'shortname' => 'VUT', + 'hasdst' => false ), + 'Pacific/Guadalcanal' => array( + 'offset' => 39600000, + 'longname' => 'Solomon Is. Time', + 'shortname' => 'SBT', + 'hasdst' => false ), + 'Pacific/Kosrae' => array( + 'offset' => 39600000, + 'longname' => 'Kosrae Time', + 'shortname' => 'KOST', + 'hasdst' => false ), + 'Pacific/Noumea' => array( + 'offset' => 39600000, + 'longname' => 'New Caledonia Time', + 'shortname' => 'NCT', + 'hasdst' => false ), + 'Pacific/Ponape' => array( + 'offset' => 39600000, + 'longname' => 'Ponape Time', + 'shortname' => 'PONT', + 'hasdst' => false ), + 'SST' => array( + 'offset' => 39600000, + 'longname' => 'Solomon Is. Time', + 'shortname' => 'SBT', + 'hasdst' => false ), + 'Pacific/Norfolk' => array( + 'offset' => 41400000, + 'longname' => 'Norfolk Time', + 'shortname' => 'NFT', + 'hasdst' => false ), + 'Antarctica/McMurdo' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'Antarctica/South_Pole' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'Asia/Anadyr' => array( + 'offset' => 43200000, + 'longname' => 'Anadyr Time', + 'shortname' => 'ANAT', + 'hasdst' => true, + 'dstlongname' => 'Anadyr Summer Time', + 'dstshortname' => 'ANAST' ), + 'Asia/Kamchatka' => array( + 'offset' => 43200000, + 'longname' => 'Petropavlovsk-Kamchatski Time', + 'shortname' => 'PETT', + 'hasdst' => true, + 'dstlongname' => 'Petropavlovsk-Kamchatski Summer Time', + 'dstshortname' => 'PETST' ), + 'Etc/GMT-12' => array( + 'offset' => 43200000, + 'longname' => 'GMT+12:00', + 'shortname' => 'GMT+12:00', + 'hasdst' => false ), + 'Kwajalein' => array( + 'offset' => 43200000, + 'longname' => 'Marshall Islands Time', + 'shortname' => 'MHT', + 'hasdst' => false ), + 'NST' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'NZ' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'Pacific/Auckland' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'Pacific/Fiji' => array( + 'offset' => 43200000, + 'longname' => 'Fiji Time', + 'shortname' => 'FJT', + 'hasdst' => false ), + 'Pacific/Funafuti' => array( + 'offset' => 43200000, + 'longname' => 'Tuvalu Time', + 'shortname' => 'TVT', + 'hasdst' => false ), + 'Pacific/Kwajalein' => array( + 'offset' => 43200000, + 'longname' => 'Marshall Islands Time', + 'shortname' => 'MHT', + 'hasdst' => false ), + 'Pacific/Majuro' => array( + 'offset' => 43200000, + 'longname' => 'Marshall Islands Time', + 'shortname' => 'MHT', + 'hasdst' => false ), + 'Pacific/Nauru' => array( + 'offset' => 43200000, + 'longname' => 'Nauru Time', + 'shortname' => 'NRT', + 'hasdst' => false ), + 'Pacific/Tarawa' => array( + 'offset' => 43200000, + 'longname' => 'Gilbert Is. Time', + 'shortname' => 'GILT', + 'hasdst' => false ), + 'Pacific/Wake' => array( + 'offset' => 43200000, + 'longname' => 'Wake Time', + 'shortname' => 'WAKT', + 'hasdst' => false ), + 'Pacific/Wallis' => array( + 'offset' => 43200000, + 'longname' => 'Wallis & Futuna Time', + 'shortname' => 'WFT', + 'hasdst' => false ), + 'NZ-CHAT' => array( + 'offset' => 45900000, + 'longname' => 'Chatham Standard Time', + 'shortname' => 'CHAST', + 'hasdst' => true, + 'dstlongname' => 'Chatham Daylight Time', + 'dstshortname' => 'CHADT' ), + 'Pacific/Chatham' => array( + 'offset' => 45900000, + 'longname' => 'Chatham Standard Time', + 'shortname' => 'CHAST', + 'hasdst' => true, + 'dstlongname' => 'Chatham Daylight Time', + 'dstshortname' => 'CHADT' ), + 'Etc/GMT-13' => array( + 'offset' => 46800000, + 'longname' => 'GMT+13:00', + 'shortname' => 'GMT+13:00', + 'hasdst' => false ), + 'Pacific/Enderbury' => array( + 'offset' => 46800000, + 'longname' => 'Phoenix Is. Time', + 'shortname' => 'PHOT', + 'hasdst' => false ), + 'Pacific/Tongatapu' => array( + 'offset' => 46800000, + 'longname' => 'Tonga Time', + 'shortname' => 'TOT', + 'hasdst' => false ), + 'Etc/GMT-14' => array( + 'offset' => 50400000, + 'longname' => 'GMT+14:00', + 'shortname' => 'GMT+14:00', + 'hasdst' => false ), + 'Pacific/Kiritimati' => array( + 'offset' => 50400000, + 'longname' => 'Line Is. Time', + 'shortname' => 'LINT', + 'hasdst' => false ), + 'GMT-12:00' => array( + 'offset' => -43200000, + 'longname' => 'GMT-12:00', + 'shortname' => 'GMT-12:00', + 'hasdst' => false ), + 'GMT-11:00' => array( + 'offset' => -39600000, + 'longname' => 'GMT-11:00', + 'shortname' => 'GMT-11:00', + 'hasdst' => false ), + 'West Samoa Time' => array( + 'offset' => -39600000, + 'longname' => 'West Samoa Time', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Samoa Standard Time' => array( + 'offset' => -39600000, + 'longname' => 'Samoa Standard Time', + 'shortname' => 'SST', + 'hasdst' => false ), + 'Niue Time' => array( + 'offset' => -39600000, + 'longname' => 'Niue Time', + 'shortname' => 'NUT', + 'hasdst' => false ), + 'Hawaii-Aleutian Standard Time' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii-Aleutian Standard Time', + 'shortname' => 'HAST', + 'hasdst' => true, + 'dstlongname' => 'Hawaii-Aleutian Daylight Time', + 'dstshortname' => 'HADT' ), + 'GMT-10:00' => array( + 'offset' => -36000000, + 'longname' => 'GMT-10:00', + 'shortname' => 'GMT-10:00', + 'hasdst' => false ), + 'Hawaii Standard Time' => array( + 'offset' => -36000000, + 'longname' => 'Hawaii Standard Time', + 'shortname' => 'HST', + 'hasdst' => false ), + 'Tokelau Time' => array( + 'offset' => -36000000, + 'longname' => 'Tokelau Time', + 'shortname' => 'TKT', + 'hasdst' => false ), + 'Cook Is. Time' => array( + 'offset' => -36000000, + 'longname' => 'Cook Is. Time', + 'shortname' => 'CKT', + 'hasdst' => false ), + 'Tahiti Time' => array( + 'offset' => -36000000, + 'longname' => 'Tahiti Time', + 'shortname' => 'TAHT', + 'hasdst' => false ), + 'Marquesas Time' => array( + 'offset' => -34200000, + 'longname' => 'Marquesas Time', + 'shortname' => 'MART', + 'hasdst' => false ), + 'Alaska Standard Time' => array( + 'offset' => -32400000, + 'longname' => 'Alaska Standard Time', + 'shortname' => 'AKST', + 'hasdst' => true, + 'dstlongname' => 'Alaska Daylight Time', + 'dstshortname' => 'AKDT' ), + 'GMT-09:00' => array( + 'offset' => -32400000, + 'longname' => 'GMT-09:00', + 'shortname' => 'GMT-09:00', + 'hasdst' => false ), + 'Gambier Time' => array( + 'offset' => -32400000, + 'longname' => 'Gambier Time', + 'shortname' => 'GAMT', + 'hasdst' => false ), + 'Pacific Standard Time' => array( + 'offset' => -28800000, + 'longname' => 'Pacific Standard Time', + 'shortname' => 'PST', + 'hasdst' => true, + 'dstlongname' => 'Pacific Daylight Time', + 'dstshortname' => 'PDT' ), + 'GMT-08:00' => array( + 'offset' => -28800000, + 'longname' => 'GMT-08:00', + 'shortname' => 'GMT-08:00', + 'hasdst' => false ), + 'Pitcairn Standard Time' => array( + 'offset' => -28800000, + 'longname' => 'Pitcairn Standard Time', + 'shortname' => 'PST', + 'hasdst' => false ), + 'Mountain Standard Time' => array( + 'offset' => -25200000, + 'longname' => 'Mountain Standard Time', + 'shortname' => 'MST', + 'hasdst' => true, + 'dstlongname' => 'Mountain Daylight Time', + 'dstshortname' => 'MDT' ), + 'GMT-07:00' => array( + 'offset' => -25200000, + 'longname' => 'GMT-07:00', + 'shortname' => 'GMT-07:00', + 'hasdst' => false ), + 'Central Standard Time' => array( + 'offset' => -18000000, + 'longname' => 'Central Standard Time', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Daylight Time', + 'dstshortname' => 'CDT' ), + 'Eastern Standard Time' => array( + 'offset' => -18000000, + 'longname' => 'Eastern Standard Time', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Daylight Time', + 'dstshortname' => 'EDT' ), + 'Easter Is. Time' => array( + 'offset' => -21600000, + 'longname' => 'Easter Is. Time', + 'shortname' => 'EAST', + 'hasdst' => true, + 'dstlongname' => 'Easter Is. Summer Time', + 'dstshortname' => 'EASST' ), + 'GMT-06:00' => array( + 'offset' => -21600000, + 'longname' => 'GMT-06:00', + 'shortname' => 'GMT-06:00', + 'hasdst' => false ), + 'Galapagos Time' => array( + 'offset' => -21600000, + 'longname' => 'Galapagos Time', + 'shortname' => 'GALT', + 'hasdst' => false ), + 'Colombia Time' => array( + 'offset' => -18000000, + 'longname' => 'Colombia Time', + 'shortname' => 'COT', + 'hasdst' => false ), + 'Acre Time' => array( + 'offset' => -18000000, + 'longname' => 'Acre Time', + 'shortname' => 'ACT', + 'hasdst' => false ), + 'Ecuador Time' => array( + 'offset' => -18000000, + 'longname' => 'Ecuador Time', + 'shortname' => 'ECT', + 'hasdst' => false ), + 'Peru Time' => array( + 'offset' => -18000000, + 'longname' => 'Peru Time', + 'shortname' => 'PET', + 'hasdst' => false ), + 'GMT-05:00' => array( + 'offset' => -18000000, + 'longname' => 'GMT-05:00', + 'shortname' => 'GMT-05:00', + 'hasdst' => false ), + 'Atlantic Standard Time' => array( + 'offset' => -14400000, + 'longname' => 'Atlantic Standard Time', + 'shortname' => 'AST', + 'hasdst' => true, + 'dstlongname' => 'Atlantic Daylight Time', + 'dstshortname' => 'ADT' ), + 'Paraguay Time' => array( + 'offset' => -14400000, + 'longname' => 'Paraguay Time', + 'shortname' => 'PYT', + 'hasdst' => true, + 'dstlongname' => 'Paraguay Summer Time', + 'dstshortname' => 'PYST' ), + 'Amazon Standard Time' => array( + 'offset' => -14400000, + 'longname' => 'Amazon Standard Time', + 'shortname' => 'AMT', + 'hasdst' => false ), + 'Venezuela Time' => array( + 'offset' => -14400000, + 'longname' => 'Venezuela Time', + 'shortname' => 'VET', + 'hasdst' => false ), + 'Guyana Time' => array( + 'offset' => -14400000, + 'longname' => 'Guyana Time', + 'shortname' => 'GYT', + 'hasdst' => false ), + 'Bolivia Time' => array( + 'offset' => -14400000, + 'longname' => 'Bolivia Time', + 'shortname' => 'BOT', + 'hasdst' => false ), + 'Chile Time' => array( + 'offset' => -14400000, + 'longname' => 'Chile Time', + 'shortname' => 'CLT', + 'hasdst' => true, + 'dstlongname' => 'Chile Summer Time', + 'dstshortname' => 'CLST' ), + 'Falkland Is. Time' => array( + 'offset' => -14400000, + 'longname' => 'Falkland Is. Time', + 'shortname' => 'FKT', + 'hasdst' => true, + 'dstlongname' => 'Falkland Is. Summer Time', + 'dstshortname' => 'FKST' ), + 'GMT-04:00' => array( + 'offset' => -14400000, + 'longname' => 'GMT-04:00', + 'shortname' => 'GMT-04:00', + 'hasdst' => false ), + 'Newfoundland Standard Time' => array( + 'offset' => -12600000, + 'longname' => 'Newfoundland Standard Time', + 'shortname' => 'NST', + 'hasdst' => true, + 'dstlongname' => 'Newfoundland Daylight Time', + 'dstshortname' => 'NDT' ), + 'Argentine Time' => array( + 'offset' => -10800000, + 'longname' => 'Argentine Time', + 'shortname' => 'ART', + 'hasdst' => false ), + 'Brazil Time' => array( + 'offset' => -10800000, + 'longname' => 'Brazil Time', + 'shortname' => 'BRT', + 'hasdst' => true, + 'dstlongname' => 'Brazil Summer Time', + 'dstshortname' => 'BRST' ), + 'French Guiana Time' => array( + 'offset' => -10800000, + 'longname' => 'French Guiana Time', + 'shortname' => 'GFT', + 'hasdst' => false ), + 'Western Greenland Time' => array( + 'offset' => -10800000, + 'longname' => 'Western Greenland Time', + 'shortname' => 'WGT', + 'hasdst' => true, + 'dstlongname' => 'Western Greenland Summer Time', + 'dstshortname' => 'WGST' ), + 'Pierre & Miquelon Standard Time' => array( + 'offset' => -10800000, + 'longname' => 'Pierre & Miquelon Standard Time', + 'shortname' => 'PMST', + 'hasdst' => true, + 'dstlongname' => 'Pierre & Miquelon Daylight Time', + 'dstshortname' => 'PMDT' ), + 'Uruguay Time' => array( + 'offset' => -10800000, + 'longname' => 'Uruguay Time', + 'shortname' => 'UYT', + 'hasdst' => false ), + 'Suriname Time' => array( + 'offset' => -10800000, + 'longname' => 'Suriname Time', + 'shortname' => 'SRT', + 'hasdst' => false ), + 'GMT-03:00' => array( + 'offset' => -10800000, + 'longname' => 'GMT-03:00', + 'shortname' => 'GMT-03:00', + 'hasdst' => false ), + 'Fernando de Noronha Time' => array( + 'offset' => -7200000, + 'longname' => 'Fernando de Noronha Time', + 'shortname' => 'FNT', + 'hasdst' => false ), + 'South Georgia Standard Time' => array( + 'offset' => -7200000, + 'longname' => 'South Georgia Standard Time', + 'shortname' => 'GST', + 'hasdst' => false ), + 'GMT-02:00' => array( + 'offset' => -7200000, + 'longname' => 'GMT-02:00', + 'shortname' => 'GMT-02:00', + 'hasdst' => false ), + 'Eastern Greenland Time' => array( + 'offset' => 3600000, + 'longname' => 'Eastern Greenland Time', + 'shortname' => 'EGT', + 'hasdst' => true, + 'dstlongname' => 'Eastern Greenland Summer Time', + 'dstshortname' => 'EGST' ), + 'Azores Time' => array( + 'offset' => -3600000, + 'longname' => 'Azores Time', + 'shortname' => 'AZOT', + 'hasdst' => true, + 'dstlongname' => 'Azores Summer Time', + 'dstshortname' => 'AZOST' ), + 'Cape Verde Time' => array( + 'offset' => -3600000, + 'longname' => 'Cape Verde Time', + 'shortname' => 'CVT', + 'hasdst' => false ), + 'GMT-01:00' => array( + 'offset' => -3600000, + 'longname' => 'GMT-01:00', + 'shortname' => 'GMT-01:00', + 'hasdst' => false ), + 'Greenwich Mean Time' => array( + 'offset' => 0, + 'longname' => 'Greenwich Mean Time', + 'shortname' => 'GMT', + 'hasdst' => false ), + 'Western European Time' => array( + 'offset' => 0, + 'longname' => 'Western European Time', + 'shortname' => 'WET', + 'hasdst' => true, + 'dstlongname' => 'Western European Summer Time', + 'dstshortname' => 'WEST' ), + 'GMT+00:00' => array( + 'offset' => 0, + 'longname' => 'GMT+00:00', + 'shortname' => 'GMT+00:00', + 'hasdst' => false ), + 'Coordinated Universal Time' => array( + 'offset' => 0, + 'longname' => 'Coordinated Universal Time', + 'shortname' => 'UTC', + 'hasdst' => false ), + 'Central European Time' => array( + 'offset' => 3600000, + 'longname' => 'Central European Time', + 'shortname' => 'CET', + 'hasdst' => true, + 'dstlongname' => 'Central European Summer Time', + 'dstshortname' => 'CEST' ), + 'Western African Time' => array( + 'offset' => 3600000, + 'longname' => 'Western African Time', + 'shortname' => 'WAT', + 'hasdst' => true, + 'dstlongname' => 'Western African Summer Time', + 'dstshortname' => 'WAST' ), + 'GMT+01:00' => array( + 'offset' => 3600000, + 'longname' => 'GMT+01:00', + 'shortname' => 'GMT+01:00', + 'hasdst' => false ), + 'Middle Europe Time' => array( + 'offset' => 3600000, + 'longname' => 'Middle Europe Time', + 'shortname' => 'MET', + 'hasdst' => true, + 'dstlongname' => 'Middle Europe Summer Time', + 'dstshortname' => 'MEST' ), + 'Eastern European Time' => array( + 'offset' => 7200000, + 'longname' => 'Eastern European Time', + 'shortname' => 'EET', + 'hasdst' => true, + 'dstlongname' => 'Eastern European Summer Time', + 'dstshortname' => 'EEST' ), + 'Central African Time' => array( + 'offset' => 7200000, + 'longname' => 'Central African Time', + 'shortname' => 'CAT', + 'hasdst' => false ), + 'South Africa Standard Time' => array( + 'offset' => 7200000, + 'longname' => 'South Africa Standard Time', + 'shortname' => 'SAST', + 'hasdst' => false ), + 'Israel Standard Time' => array( + 'offset' => 7200000, + 'longname' => 'Israel Standard Time', + 'shortname' => 'IST', + 'hasdst' => true, + 'dstlongname' => 'Israel Daylight Time', + 'dstshortname' => 'IDT' ), + 'GMT+02:00' => array( + 'offset' => 7200000, + 'longname' => 'GMT+02:00', + 'shortname' => 'GMT+02:00', + 'hasdst' => false ), + 'Eastern African Time' => array( + 'offset' => 10800000, + 'longname' => 'Eastern African Time', + 'shortname' => 'EAT', + 'hasdst' => false ), + 'Syowa Time' => array( + 'offset' => 10800000, + 'longname' => 'Syowa Time', + 'shortname' => 'SYOT', + 'hasdst' => false ), + 'Arabia Standard Time' => array( + 'offset' => 10800000, + 'longname' => 'Arabia Standard Time', + 'shortname' => 'AST', + 'hasdst' => false ), + 'GMT+03:00' => array( + 'offset' => 10800000, + 'longname' => 'GMT+03:00', + 'shortname' => 'GMT+03:00', + 'hasdst' => false ), + 'Moscow Standard Time' => array( + 'offset' => 10800000, + 'longname' => 'Moscow Standard Time', + 'shortname' => 'MSK', + 'hasdst' => true, + 'dstlongname' => 'Moscow Daylight Time', + 'dstshortname' => 'MSD' ), + 'GMT+03:07' => array( + 'offset' => 11224000, + 'longname' => 'GMT+03:07', + 'shortname' => 'GMT+03:07', + 'hasdst' => false ), + 'Iran Time' => array( + 'offset' => 12600000, + 'longname' => 'Iran Time', + 'shortname' => 'IRT', + 'hasdst' => true, + 'dstlongname' => 'Iran Sumer Time', + 'dstshortname' => 'IRST' ), + 'Aqtau Time' => array( + 'offset' => 14400000, + 'longname' => 'Aqtau Time', + 'shortname' => 'AQTT', + 'hasdst' => true, + 'dstlongname' => 'Aqtau Summer Time', + 'dstshortname' => 'AQTST' ), + 'Azerbaijan Time' => array( + 'offset' => 14400000, + 'longname' => 'Azerbaijan Time', + 'shortname' => 'AZT', + 'hasdst' => true, + 'dstlongname' => 'Azerbaijan Summer Time', + 'dstshortname' => 'AZST' ), + 'Gulf Standard Time' => array( + 'offset' => 14400000, + 'longname' => 'Gulf Standard Time', + 'shortname' => 'GST', + 'hasdst' => false ), + 'Georgia Time' => array( + 'offset' => 14400000, + 'longname' => 'Georgia Time', + 'shortname' => 'GET', + 'hasdst' => true, + 'dstlongname' => 'Georgia Summer Time', + 'dstshortname' => 'GEST' ), + 'Armenia Time' => array( + 'offset' => 14400000, + 'longname' => 'Armenia Time', + 'shortname' => 'AMT', + 'hasdst' => true, + 'dstlongname' => 'Armenia Summer Time', + 'dstshortname' => 'AMST' ), + 'GMT+04:00' => array( + 'offset' => 14400000, + 'longname' => 'GMT+04:00', + 'shortname' => 'GMT+04:00', + 'hasdst' => false ), + 'Samara Time' => array( + 'offset' => 14400000, + 'longname' => 'Samara Time', + 'shortname' => 'SAMT', + 'hasdst' => true, + 'dstlongname' => 'Samara Summer Time', + 'dstshortname' => 'SAMST' ), + 'Seychelles Time' => array( + 'offset' => 14400000, + 'longname' => 'Seychelles Time', + 'shortname' => 'SCT', + 'hasdst' => false ), + 'Mauritius Time' => array( + 'offset' => 14400000, + 'longname' => 'Mauritius Time', + 'shortname' => 'MUT', + 'hasdst' => false ), + 'Reunion Time' => array( + 'offset' => 14400000, + 'longname' => 'Reunion Time', + 'shortname' => 'RET', + 'hasdst' => false ), + 'Afghanistan Time' => array( + 'offset' => 16200000, + 'longname' => 'Afghanistan Time', + 'shortname' => 'AFT', + 'hasdst' => false ), + 'Aqtobe Time' => array( + 'offset' => 18000000, + 'longname' => 'Aqtobe Time', + 'shortname' => 'AQTT', + 'hasdst' => true, + 'dstlongname' => 'Aqtobe Summer Time', + 'dstshortname' => 'AQTST' ), + 'Turkmenistan Time' => array( + 'offset' => 18000000, + 'longname' => 'Turkmenistan Time', + 'shortname' => 'TMT', + 'hasdst' => false ), + 'Kirgizstan Time' => array( + 'offset' => 18000000, + 'longname' => 'Kirgizstan Time', + 'shortname' => 'KGT', + 'hasdst' => true, + 'dstlongname' => 'Kirgizstan Summer Time', + 'dstshortname' => 'KGST' ), + 'Tajikistan Time' => array( + 'offset' => 18000000, + 'longname' => 'Tajikistan Time', + 'shortname' => 'TJT', + 'hasdst' => false ), + 'Pakistan Time' => array( + 'offset' => 18000000, + 'longname' => 'Pakistan Time', + 'shortname' => 'PKT', + 'hasdst' => false ), + 'Uzbekistan Time' => array( + 'offset' => 18000000, + 'longname' => 'Uzbekistan Time', + 'shortname' => 'UZT', + 'hasdst' => false ), + 'Yekaterinburg Time' => array( + 'offset' => 18000000, + 'longname' => 'Yekaterinburg Time', + 'shortname' => 'YEKT', + 'hasdst' => true, + 'dstlongname' => 'Yekaterinburg Summer Time', + 'dstshortname' => 'YEKST' ), + 'GMT+05:00' => array( + 'offset' => 18000000, + 'longname' => 'GMT+05:00', + 'shortname' => 'GMT+05:00', + 'hasdst' => false ), + 'French Southern & Antarctic Lands Time' => array( + 'offset' => 18000000, + 'longname' => 'French Southern & Antarctic Lands Time', + 'shortname' => 'TFT', + 'hasdst' => false ), + 'Maldives Time' => array( + 'offset' => 18000000, + 'longname' => 'Maldives Time', + 'shortname' => 'MVT', + 'hasdst' => false ), + 'India Standard Time' => array( + 'offset' => 19800000, + 'longname' => 'India Standard Time', + 'shortname' => 'IST', + 'hasdst' => false ), + 'Nepal Time' => array( + 'offset' => 20700000, + 'longname' => 'Nepal Time', + 'shortname' => 'NPT', + 'hasdst' => false ), + 'Mawson Time' => array( + 'offset' => 21600000, + 'longname' => 'Mawson Time', + 'shortname' => 'MAWT', + 'hasdst' => false ), + 'Vostok time' => array( + 'offset' => 21600000, + 'longname' => 'Vostok time', + 'shortname' => 'VOST', + 'hasdst' => false ), + 'Alma-Ata Time' => array( + 'offset' => 21600000, + 'longname' => 'Alma-Ata Time', + 'shortname' => 'ALMT', + 'hasdst' => true, + 'dstlongname' => 'Alma-Ata Summer Time', + 'dstshortname' => 'ALMST' ), + 'Sri Lanka Time' => array( + 'offset' => 21600000, + 'longname' => 'Sri Lanka Time', + 'shortname' => 'LKT', + 'hasdst' => false ), + 'Bangladesh Time' => array( + 'offset' => 21600000, + 'longname' => 'Bangladesh Time', + 'shortname' => 'BDT', + 'hasdst' => false ), + 'Novosibirsk Time' => array( + 'offset' => 21600000, + 'longname' => 'Novosibirsk Time', + 'shortname' => 'NOVT', + 'hasdst' => true, + 'dstlongname' => 'Novosibirsk Summer Time', + 'dstshortname' => 'NOVST' ), + 'Omsk Time' => array( + 'offset' => 21600000, + 'longname' => 'Omsk Time', + 'shortname' => 'OMST', + 'hasdst' => true, + 'dstlongname' => 'Omsk Summer Time', + 'dstshortname' => 'OMSST' ), + 'Bhutan Time' => array( + 'offset' => 21600000, + 'longname' => 'Bhutan Time', + 'shortname' => 'BTT', + 'hasdst' => false ), + 'GMT+06:00' => array( + 'offset' => 21600000, + 'longname' => 'GMT+06:00', + 'shortname' => 'GMT+06:00', + 'hasdst' => false ), + 'Indian Ocean Territory Time' => array( + 'offset' => 21600000, + 'longname' => 'Indian Ocean Territory Time', + 'shortname' => 'IOT', + 'hasdst' => false ), + 'Myanmar Time' => array( + 'offset' => 23400000, + 'longname' => 'Myanmar Time', + 'shortname' => 'MMT', + 'hasdst' => false ), + 'Cocos Islands Time' => array( + 'offset' => 23400000, + 'longname' => 'Cocos Islands Time', + 'shortname' => 'CCT', + 'hasdst' => false ), + 'Davis Time' => array( + 'offset' => 25200000, + 'longname' => 'Davis Time', + 'shortname' => 'DAVT', + 'hasdst' => false ), + 'Indochina Time' => array( + 'offset' => 25200000, + 'longname' => 'Indochina Time', + 'shortname' => 'ICT', + 'hasdst' => false ), + 'Hovd Time' => array( + 'offset' => 25200000, + 'longname' => 'Hovd Time', + 'shortname' => 'HOVT', + 'hasdst' => false ), + 'West Indonesia Time' => array( + 'offset' => 25200000, + 'longname' => 'West Indonesia Time', + 'shortname' => 'WIT', + 'hasdst' => false ), + 'Krasnoyarsk Time' => array( + 'offset' => 25200000, + 'longname' => 'Krasnoyarsk Time', + 'shortname' => 'KRAT', + 'hasdst' => true, + 'dstlongname' => 'Krasnoyarsk Summer Time', + 'dstshortname' => 'KRAST' ), + 'GMT+07:00' => array( + 'offset' => 25200000, + 'longname' => 'GMT+07:00', + 'shortname' => 'GMT+07:00', + 'hasdst' => false ), + 'Christmas Island Time' => array( + 'offset' => 25200000, + 'longname' => 'Christmas Island Time', + 'shortname' => 'CXT', + 'hasdst' => false ), + 'Western Standard Time (Australia)' => array( + 'offset' => 28800000, + 'longname' => 'Western Standard Time (Australia)', + 'shortname' => 'WST', + 'hasdst' => false ), + 'Brunei Time' => array( + 'offset' => 28800000, + 'longname' => 'Brunei Time', + 'shortname' => 'BNT', + 'hasdst' => false ), + 'China Standard Time' => array( + 'offset' => 28800000, + 'longname' => 'China Standard Time', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Hong Kong Time' => array( + 'offset' => 28800000, + 'longname' => 'Hong Kong Time', + 'shortname' => 'HKT', + 'hasdst' => false ), + 'Irkutsk Time' => array( + 'offset' => 28800000, + 'longname' => 'Irkutsk Time', + 'shortname' => 'IRKT', + 'hasdst' => true, + 'dstlongname' => 'Irkutsk Summer Time', + 'dstshortname' => 'IRKST' ), + 'Malaysia Time' => array( + 'offset' => 28800000, + 'longname' => 'Malaysia Time', + 'shortname' => 'MYT', + 'hasdst' => false ), + 'Philippines Time' => array( + 'offset' => 28800000, + 'longname' => 'Philippines Time', + 'shortname' => 'PHT', + 'hasdst' => false ), + 'Singapore Time' => array( + 'offset' => 28800000, + 'longname' => 'Singapore Time', + 'shortname' => 'SGT', + 'hasdst' => false ), + 'Central Indonesia Time' => array( + 'offset' => 28800000, + 'longname' => 'Central Indonesia Time', + 'shortname' => 'CIT', + 'hasdst' => false ), + 'Ulaanbaatar Time' => array( + 'offset' => 28800000, + 'longname' => 'Ulaanbaatar Time', + 'shortname' => 'ULAT', + 'hasdst' => false ), + 'GMT+08:00' => array( + 'offset' => 28800000, + 'longname' => 'GMT+08:00', + 'shortname' => 'GMT+08:00', + 'hasdst' => false ), + 'Choibalsan Time' => array( + 'offset' => 32400000, + 'longname' => 'Choibalsan Time', + 'shortname' => 'CHOT', + 'hasdst' => false ), + 'East Timor Time' => array( + 'offset' => 32400000, + 'longname' => 'East Timor Time', + 'shortname' => 'TPT', + 'hasdst' => false ), + 'East Indonesia Time' => array( + 'offset' => 32400000, + 'longname' => 'East Indonesia Time', + 'shortname' => 'EIT', + 'hasdst' => false ), + 'Korea Standard Time' => array( + 'offset' => 32400000, + 'longname' => 'Korea Standard Time', + 'shortname' => 'KST', + 'hasdst' => false ), + 'Japan Standard Time' => array( + 'offset' => 32400000, + 'longname' => 'Japan Standard Time', + 'shortname' => 'JST', + 'hasdst' => false ), + 'Yakutsk Time' => array( + 'offset' => 32400000, + 'longname' => 'Yakutsk Time', + 'shortname' => 'YAKT', + 'hasdst' => true, + 'dstlongname' => 'Yaktsk Summer Time', + 'dstshortname' => 'YAKST' ), + 'GMT+09:00' => array( + 'offset' => 32400000, + 'longname' => 'GMT+09:00', + 'shortname' => 'GMT+09:00', + 'hasdst' => false ), + 'Palau Time' => array( + 'offset' => 32400000, + 'longname' => 'Palau Time', + 'shortname' => 'PWT', + 'hasdst' => false ), + 'Central Standard Time (Northern Territory)' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (Northern Territory)', + 'shortname' => 'CST', + 'hasdst' => false ), + 'Central Standard Time (South Australia)' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia)', + 'dstshortname' => 'CST' ), + 'Central Standard Time (South Australia/New South Wales)' => array( + 'offset' => 34200000, + 'longname' => 'Central Standard Time (South Australia/New South Wales)', + 'shortname' => 'CST', + 'hasdst' => true, + 'dstlongname' => 'Central Summer Time (South Australia/New South Wales)', + 'dstshortname' => 'CST' ), + 'Eastern Standard Time (New South Wales)' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (New South Wales)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (New South Wales)', + 'dstshortname' => 'EST' ), + 'Dumont-d\'Urville Time' => array( + 'offset' => 36000000, + 'longname' => 'Dumont-d\'Urville Time', + 'shortname' => 'DDUT', + 'hasdst' => false ), + 'Sakhalin Time' => array( + 'offset' => 36000000, + 'longname' => 'Sakhalin Time', + 'shortname' => 'SAKT', + 'hasdst' => true, + 'dstlongname' => 'Sakhalin Summer Time', + 'dstshortname' => 'SAKST' ), + 'Vladivostok Time' => array( + 'offset' => 36000000, + 'longname' => 'Vladivostok Time', + 'shortname' => 'VLAT', + 'hasdst' => true, + 'dstlongname' => 'Vladivostok Summer Time', + 'dstshortname' => 'VLAST' ), + 'Eastern Standard Time (Queensland)' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Queensland)', + 'shortname' => 'EST', + 'hasdst' => false ), + 'Eastern Standard Time (Tasmania)' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Tasmania)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Tasmania)', + 'dstshortname' => 'EST' ), + 'Eastern Standard Time (Victoria)' => array( + 'offset' => 36000000, + 'longname' => 'Eastern Standard Time (Victoria)', + 'shortname' => 'EST', + 'hasdst' => true, + 'dstlongname' => 'Eastern Summer Time (Victoria)', + 'dstshortname' => 'EST' ), + 'GMT+10:00' => array( + 'offset' => 36000000, + 'longname' => 'GMT+10:00', + 'shortname' => 'GMT+10:00', + 'hasdst' => false ), + 'Chamorro Standard Time' => array( + 'offset' => 36000000, + 'longname' => 'Chamorro Standard Time', + 'shortname' => 'ChST', + 'hasdst' => false ), + 'Papua New Guinea Time' => array( + 'offset' => 36000000, + 'longname' => 'Papua New Guinea Time', + 'shortname' => 'PGT', + 'hasdst' => false ), + 'Truk Time' => array( + 'offset' => 36000000, + 'longname' => 'Truk Time', + 'shortname' => 'TRUT', + 'hasdst' => false ), + 'Yap Time' => array( + 'offset' => 36000000, + 'longname' => 'Yap Time', + 'shortname' => 'YAPT', + 'hasdst' => false ), + 'Load Howe Standard Time' => array( + 'offset' => 37800000, + 'longname' => 'Load Howe Standard Time', + 'shortname' => 'LHST', + 'hasdst' => true, + 'dstlongname' => 'Load Howe Summer Time', + 'dstshortname' => 'LHST' ), + 'Magadan Time' => array( + 'offset' => 39600000, + 'longname' => 'Magadan Time', + 'shortname' => 'MAGT', + 'hasdst' => true, + 'dstlongname' => 'Magadan Summer Time', + 'dstshortname' => 'MAGST' ), + 'GMT+11:00' => array( + 'offset' => 39600000, + 'longname' => 'GMT+11:00', + 'shortname' => 'GMT+11:00', + 'hasdst' => false ), + 'Vanuatu Time' => array( + 'offset' => 39600000, + 'longname' => 'Vanuatu Time', + 'shortname' => 'VUT', + 'hasdst' => false ), + 'Solomon Is. Time' => array( + 'offset' => 39600000, + 'longname' => 'Solomon Is. Time', + 'shortname' => 'SBT', + 'hasdst' => false ), + 'Kosrae Time' => array( + 'offset' => 39600000, + 'longname' => 'Kosrae Time', + 'shortname' => 'KOST', + 'hasdst' => false ), + 'New Caledonia Time' => array( + 'offset' => 39600000, + 'longname' => 'New Caledonia Time', + 'shortname' => 'NCT', + 'hasdst' => false ), + 'Ponape Time' => array( + 'offset' => 39600000, + 'longname' => 'Ponape Time', + 'shortname' => 'PONT', + 'hasdst' => false ), + 'Norfolk Time' => array( + 'offset' => 41400000, + 'longname' => 'Norfolk Time', + 'shortname' => 'NFT', + 'hasdst' => false ), + 'New Zealand Standard Time' => array( + 'offset' => 43200000, + 'longname' => 'New Zealand Standard Time', + 'shortname' => 'NZST', + 'hasdst' => true, + 'dstlongname' => 'New Zealand Daylight Time', + 'dstshortname' => 'NZDT' ), + 'Anadyr Time' => array( + 'offset' => 43200000, + 'longname' => 'Anadyr Time', + 'shortname' => 'ANAT', + 'hasdst' => true, + 'dstlongname' => 'Anadyr Summer Time', + 'dstshortname' => 'ANAST' ), + 'Petropavlovsk-Kamchatski Time' => array( + 'offset' => 43200000, + 'longname' => 'Petropavlovsk-Kamchatski Time', + 'shortname' => 'PETT', + 'hasdst' => true, + 'dstlongname' => 'Petropavlovsk-Kamchatski Summer Time', + 'dstshortname' => 'PETST' ), + 'GMT+12:00' => array( + 'offset' => 43200000, + 'longname' => 'GMT+12:00', + 'shortname' => 'GMT+12:00', + 'hasdst' => false ), + 'Marshall Islands Time' => array( + 'offset' => 43200000, + 'longname' => 'Marshall Islands Time', + 'shortname' => 'MHT', + 'hasdst' => false ), + 'Fiji Time' => array( + 'offset' => 43200000, + 'longname' => 'Fiji Time', + 'shortname' => 'FJT', + 'hasdst' => false ), + 'Tuvalu Time' => array( + 'offset' => 43200000, + 'longname' => 'Tuvalu Time', + 'shortname' => 'TVT', + 'hasdst' => false ), + 'Nauru Time' => array( + 'offset' => 43200000, + 'longname' => 'Nauru Time', + 'shortname' => 'NRT', + 'hasdst' => false ), + 'Gilbert Is. Time' => array( + 'offset' => 43200000, + 'longname' => 'Gilbert Is. Time', + 'shortname' => 'GILT', + 'hasdst' => false ), + 'Wake Time' => array( + 'offset' => 43200000, + 'longname' => 'Wake Time', + 'shortname' => 'WAKT', + 'hasdst' => false ), + 'Wallis & Futuna Time' => array( + 'offset' => 43200000, + 'longname' => 'Wallis & Futuna Time', + 'shortname' => 'WFT', + 'hasdst' => false ), + 'Chatham Standard Time' => array( + 'offset' => 45900000, + 'longname' => 'Chatham Standard Time', + 'shortname' => 'CHAST', + 'hasdst' => true, + 'dstlongname' => 'Chatham Daylight Time', + 'dstshortname' => 'CHADT' ), + 'GMT+13:00' => array( + 'offset' => 46800000, + 'longname' => 'GMT+13:00', + 'shortname' => 'GMT+13:00', + 'hasdst' => false ), + 'Phoenix Is. Time' => array( + 'offset' => 46800000, + 'longname' => 'Phoenix Is. Time', + 'shortname' => 'PHOT', + 'hasdst' => false ), + 'Tonga Time' => array( + 'offset' => 46800000, + 'longname' => 'Tonga Time', + 'shortname' => 'TOT', + 'hasdst' => false ), + 'GMT+14:00' => array( + 'offset' => 50400000, + 'longname' => 'GMT+14:00', + 'shortname' => 'GMT+14:00', + 'hasdst' => false ), + 'Line Is. Time' => array( + 'offset' => 50400000, + 'longname' => 'Line Is. Time', + 'shortname' => 'LINT', + 'hasdst' => false ), +); + +/** + * Initialize default timezone + * + * First try _DATE_TIMEZONE_DEFAULT global, then PHP_TZ environment var, + * then TZ environment var + */ +if(isset($GLOBALS['_DATE_TIMEZONE_DEFAULT']) + && Date_TimeZone::isValidID($GLOBALS['_DATE_TIMEZONE_DEFAULT'])) +{ + Date_TimeZone::setDefault($GLOBALS['_DATE_TIMEZONE_DEFAULT']); +} elseif (getenv('PHP_TZ') && Date_TimeZone::isValidID(getenv('PHP_TZ'))) { + Date_TimeZone::setDefault(getenv('PHP_TZ')); +} elseif (getenv('TZ') && Date_TimeZone::isValidID(getenv('TZ'))) { + Date_TimeZone::setDefault(getenv('TZ')); +} elseif (Date_TimeZone::isValidID(date('T'))) { + Date_TimeZone::setDefault(date('T')); +} else { + Date_TimeZone::setDefault('UTC'); +} + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ +?> \ No newline at end of file diff --git a/HTML/Common.php b/HTML/Common.php new file mode 100644 index 0000000..9b7bae4 --- /dev/null +++ b/HTML/Common.php @@ -0,0 +1,464 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: Common.php,v 1.14 2007/05/16 20:06:44 avb Exp $ + * @link http://pear.php.net/package/HTML_Common/ + */ + +/** + * Base class for all HTML classes + * + * @category HTML + * @package HTML_Common + * @author Adam Daniel + * @version Release: 1.2.4 + * @abstract + */ +class HTML_Common +{ + /** + * Associative array of attributes + * @var array + * @access private + */ + var $_attributes = array(); + + /** + * Tab offset of the tag + * @var int + * @access private + */ + var $_tabOffset = 0; + + /** + * Tab string + * @var string + * @since 1.7 + * @access private + */ + var $_tab = "\11"; + + /** + * Contains the line end string + * @var string + * @since 1.7 + * @access private + */ + var $_lineEnd = "\12"; + + /** + * HTML comment on the object + * @var string + * @since 1.5 + * @access private + */ + var $_comment = ''; + + /** + * Class constructor + * @param mixed $attributes Associative array of table tag attributes + * or HTML attributes name="value" pairs + * @param int $tabOffset Indent offset in tabs + * @access public + */ + function HTML_Common($attributes = null, $tabOffset = 0) + { + $this->setAttributes($attributes); + $this->setTabOffset($tabOffset); + } // end constructor + + /** + * Returns the current API version + * @access public + * @returns double + */ + function apiVersion() + { + return 1.7; + } // end func apiVersion + + /** + * Returns the lineEnd + * + * @since 1.7 + * @access private + * @return string + */ + function _getLineEnd() + { + return $this->_lineEnd; + } // end func getLineEnd + + /** + * Returns a string containing the unit for indenting HTML + * + * @since 1.7 + * @access private + * @return string + */ + function _getTab() + { + return $this->_tab; + } // end func _getTab + + /** + * Returns a string containing the offset for the whole HTML code + * + * @return string + * @access private + */ + function _getTabs() + { + return str_repeat($this->_getTab(), $this->_tabOffset); + } // end func _getTabs + + /** + * Returns an HTML formatted attribute string + * @param array $attributes + * @return string + * @access private + */ + function _getAttrString($attributes) + { + $strAttr = ''; + + if (is_array($attributes)) { + $charset = HTML_Common::charset(); + foreach ($attributes as $key => $value) { + $strAttr .= ' ' . $key . '="' . htmlspecialchars($value, ENT_COMPAT, $charset) . '"'; + } + } + return $strAttr; + } // end func _getAttrString + + /** + * Returns a valid atrributes array from either a string or array + * @param mixed $attributes Either a typical HTML attribute string or an associative array + * @access private + * @return array + */ + function _parseAttributes($attributes) + { + if (is_array($attributes)) { + $ret = array(); + foreach ($attributes as $key => $value) { + if (is_int($key)) { + $key = $value = strtolower($value); + } else { + $key = strtolower($key); + } + $ret[$key] = $value; + } + return $ret; + + } elseif (is_string($attributes)) { + $preg = "/(([A-Za-z_:]|[^\\x00-\\x7F])([A-Za-z0-9_:.-]|[^\\x00-\\x7F])*)" . + "([ \\n\\t\\r]+)?(=([ \\n\\t\\r]+)?(\"[^\"]*\"|'[^']*'|[^ \\n\\t\\r]*))?/"; + if (preg_match_all($preg, $attributes, $regs)) { + for ($counter=0; $counter $value) { + $attr1[$key] = $value; + } + } // end func _updateAtrrArray + + /** + * Removes the given attribute from the given array + * + * @param string $attr Attribute name + * @param array $attributes Attribute array + * @since 1.4 + * @access private + * @return void + */ + function _removeAttr($attr, &$attributes) + { + $attr = strtolower($attr); + if (isset($attributes[$attr])) { + unset($attributes[$attr]); + } + } //end func _removeAttr + + /** + * Returns the value of the given attribute + * + * @param string $attr Attribute name + * @since 1.5 + * @access public + * @return string|null returns null if an attribute does not exist + */ + function getAttribute($attr) + { + $attr = strtolower($attr); + if (isset($this->_attributes[$attr])) { + return $this->_attributes[$attr]; + } + return null; + } //end func getAttribute + + /** + * Sets the value of the attribute + * + * @param string Attribute name + * @param string Attribute value (will be set to $name if omitted) + * @access public + */ + function setAttribute($name, $value = null) + { + $name = strtolower($name); + if (is_null($value)) { + $value = $name; + } + $this->_attributes[$name] = $value; + } // end func setAttribute + + /** + * Sets the HTML attributes + * @param mixed $attributes Either a typical HTML attribute string or an associative array + * @access public + */ + function setAttributes($attributes) + { + $this->_attributes = $this->_parseAttributes($attributes); + } // end func setAttributes + + /** + * Returns the assoc array (default) or string of attributes + * + * @param bool Whether to return the attributes as string + * @since 1.6 + * @access public + * @return mixed attributes + */ + function getAttributes($asString = false) + { + if ($asString) { + return $this->_getAttrString($this->_attributes); + } else { + return $this->_attributes; + } + } //end func getAttributes + + /** + * Updates the passed attributes without changing the other existing attributes + * @param mixed $attributes Either a typical HTML attribute string or an associative array + * @access public + */ + function updateAttributes($attributes) + { + $this->_updateAttrArray($this->_attributes, $this->_parseAttributes($attributes)); + } // end func updateAttributes + + /** + * Removes an attribute + * + * @param string $attr Attribute name + * @since 1.4 + * @access public + * @return void + */ + function removeAttribute($attr) + { + $this->_removeAttr($attr, $this->_attributes); + } //end func removeAttribute + + /** + * Sets the line end style to Windows, Mac, Unix or a custom string. + * + * @param string $style "win", "mac", "unix" or custom string. + * @since 1.7 + * @access public + * @return void + */ + function setLineEnd($style) + { + switch ($style) { + case 'win': + $this->_lineEnd = "\15\12"; + break; + case 'unix': + $this->_lineEnd = "\12"; + break; + case 'mac': + $this->_lineEnd = "\15"; + break; + default: + $this->_lineEnd = $style; + } + } // end func setLineEnd + + /** + * Sets the tab offset + * + * @param int $offset + * @access public + */ + function setTabOffset($offset) + { + $this->_tabOffset = $offset; + } // end func setTabOffset + + /** + * Returns the tabOffset + * + * @since 1.5 + * @access public + * @return int + */ + function getTabOffset() + { + return $this->_tabOffset; + } //end func getTabOffset + + /** + * Sets the string used to indent HTML + * + * @since 1.7 + * @param string $string String used to indent ("\11", "\t", ' ', etc.). + * @access public + * @return void + */ + function setTab($string) + { + $this->_tab = $string; + } // end func setTab + + /** + * Sets the HTML comment to be displayed at the beginning of the HTML string + * + * @param string + * @since 1.4 + * @access public + * @return void + */ + function setComment($comment) + { + $this->_comment = $comment; + } // end func setHtmlComment + + /** + * Returns the HTML comment + * + * @since 1.5 + * @access public + * @return string + */ + function getComment() + { + return $this->_comment; + } //end func getComment + + /** + * Abstract method. Must be extended to return the objects HTML + * + * @access public + * @return string + * @abstract + */ + function toHtml() + { + return ''; + } // end func toHtml + + /** + * Displays the HTML to the screen + * + * @access public + */ + function display() + { + print $this->toHtml(); + } // end func display + + /** + * Sets the charset to use by htmlspecialchars() function + * + * Since this parameter is expected to be global, the function is designed + * to be called statically: + * + * HTML_Common::charset('utf-8'); + * + * or + * + * $charset = HTML_Common::charset(); + * + * + * @param string New charset to use. Omit if just getting the + * current value. Consult the htmlspecialchars() docs + * for a list of supported character sets. + * @return string Current charset + * @access public + * @static + */ + function charset($newCharset = null) + { + static $charset = 'ISO-8859-1'; + + if (!is_null($newCharset)) { + $charset = $newCharset; + } + return $charset; + } // end func charset +} // end class HTML_Common +?> diff --git a/HTML/QuickForm.php b/HTML/QuickForm.php new file mode 100644 index 0000000..f32ad07 --- /dev/null +++ b/HTML/QuickForm.php @@ -0,0 +1,2054 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: QuickForm.php,v 1.163 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * PEAR and PEAR_Error classes, for error handling + */ +require_once 'PEAR.php'; +/** + * Base class for all HTML classes + */ +require_once 'HTML/Common.php'; + +/** + * Element types known to HTML_QuickForm + * @see HTML_QuickForm::registerElementType(), HTML_QuickForm::getRegisteredTypes(), + * HTML_QuickForm::isTypeRegistered() + * @global array $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'] + */ +$GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'] = + array( + 'group' =>array('HTML/QuickForm/group.php','HTML_QuickForm_group'), + 'hidden' =>array('HTML/QuickForm/hidden.php','HTML_QuickForm_hidden'), + 'reset' =>array('HTML/QuickForm/reset.php','HTML_QuickForm_reset'), + 'checkbox' =>array('HTML/QuickForm/checkbox.php','HTML_QuickForm_checkbox'), + 'file' =>array('HTML/QuickForm/file.php','HTML_QuickForm_file'), + 'image' =>array('HTML/QuickForm/image.php','HTML_QuickForm_image'), + 'password' =>array('HTML/QuickForm/password.php','HTML_QuickForm_password'), + 'radio' =>array('HTML/QuickForm/radio.php','HTML_QuickForm_radio'), + 'button' =>array('HTML/QuickForm/button.php','HTML_QuickForm_button'), + 'submit' =>array('HTML/QuickForm/submit.php','HTML_QuickForm_submit'), + 'select' =>array('HTML/QuickForm/select.php','HTML_QuickForm_select'), + 'hiddenselect' =>array('HTML/QuickForm/hiddenselect.php','HTML_QuickForm_hiddenselect'), + 'text' =>array('HTML/QuickForm/text.php','HTML_QuickForm_text'), + 'textarea' =>array('HTML/QuickForm/textarea.php','HTML_QuickForm_textarea'), + 'link' =>array('HTML/QuickForm/link.php','HTML_QuickForm_link'), + 'advcheckbox' =>array('HTML/QuickForm/advcheckbox.php','HTML_QuickForm_advcheckbox'), + 'date' =>array('HTML/QuickForm/date.php','HTML_QuickForm_date'), + 'static' =>array('HTML/QuickForm/static.php','HTML_QuickForm_static'), + 'header' =>array('HTML/QuickForm/header.php', 'HTML_QuickForm_header'), + 'html' =>array('HTML/QuickForm/html.php', 'HTML_QuickForm_html'), + 'hierselect' =>array('HTML/QuickForm/hierselect.php', 'HTML_QuickForm_hierselect'), + 'autocomplete' =>array('HTML/QuickForm/autocomplete.php', 'HTML_QuickForm_autocomplete'), + 'xbutton' =>array('HTML/QuickForm/xbutton.php','HTML_QuickForm_xbutton') + ); + +/** + * Validation rules known to HTML_QuickForm + * @see HTML_QuickForm::registerRule(), HTML_QuickForm::getRegisteredRules(), + * HTML_QuickForm::isRuleRegistered() + * @global array $GLOBALS['_HTML_QuickForm_registered_rules'] + */ +$GLOBALS['_HTML_QuickForm_registered_rules'] = array( + 'required' => array('html_quickform_rule_required', 'HTML/QuickForm/Rule/Required.php'), + 'maxlength' => array('html_quickform_rule_range', 'HTML/QuickForm/Rule/Range.php'), + 'minlength' => array('html_quickform_rule_range', 'HTML/QuickForm/Rule/Range.php'), + 'rangelength' => array('html_quickform_rule_range', 'HTML/QuickForm/Rule/Range.php'), + 'email' => array('html_quickform_rule_email', 'HTML/QuickForm/Rule/Email.php'), + 'regex' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'lettersonly' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'alphanumeric' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'numeric' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'nopunctuation' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'nonzero' => array('html_quickform_rule_regex', 'HTML/QuickForm/Rule/Regex.php'), + 'callback' => array('html_quickform_rule_callback', 'HTML/QuickForm/Rule/Callback.php'), + 'compare' => array('html_quickform_rule_compare', 'HTML/QuickForm/Rule/Compare.php') +); + +// {{{ error codes + +/**#@+ + * Error codes for HTML_QuickForm + * + * Codes are mapped to textual messages by errorMessage() method, if you add a + * new code be sure to add a new message for it to errorMessage() + * + * @see HTML_QuickForm::errorMessage() + */ +define('QUICKFORM_OK', 1); +define('QUICKFORM_ERROR', -1); +define('QUICKFORM_INVALID_RULE', -2); +define('QUICKFORM_NONEXIST_ELEMENT', -3); +define('QUICKFORM_INVALID_FILTER', -4); +define('QUICKFORM_UNREGISTERED_ELEMENT', -5); +define('QUICKFORM_INVALID_ELEMENT_NAME', -6); +define('QUICKFORM_INVALID_PROCESS', -7); +define('QUICKFORM_DEPRECATED', -8); +define('QUICKFORM_INVALID_DATASOURCE', -9); +/**#@-*/ + +// }}} + +/** + * Create, validate and process HTML forms + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + */ +class HTML_QuickForm extends HTML_Common +{ + // {{{ properties + + /** + * Array containing the form fields + * @since 1.0 + * @var array + * @access private + */ + var $_elements = array(); + + /** + * Array containing element name to index map + * @since 1.1 + * @var array + * @access private + */ + var $_elementIndex = array(); + + /** + * Array containing indexes of duplicate elements + * @since 2.10 + * @var array + * @access private + */ + var $_duplicateIndex = array(); + + /** + * Array containing required field IDs + * @since 1.0 + * @var array + * @access private + */ + var $_required = array(); + + /** + * Prefix message in javascript alert if error + * @since 1.0 + * @var string + * @access public + */ + var $_jsPrefix = 'Invalid information entered.'; + + /** + * Postfix message in javascript alert if error + * @since 1.0 + * @var string + * @access public + */ + var $_jsPostfix = 'Please correct these fields.'; + + /** + * Datasource object implementing the informal + * datasource protocol + * @since 3.3 + * @var object + * @access private + */ + var $_datasource; + + /** + * Array of default form values + * @since 2.0 + * @var array + * @access private + */ + var $_defaultValues = array(); + + /** + * Array of constant form values + * @since 2.0 + * @var array + * @access private + */ + var $_constantValues = array(); + + /** + * Array of submitted form values + * @since 1.0 + * @var array + * @access private + */ + var $_submitValues = array(); + + /** + * Array of submitted form files + * @since 1.0 + * @var integer + * @access public + */ + var $_submitFiles = array(); + + /** + * Value for maxfilesize hidden element if form contains file input + * @since 1.0 + * @var integer + * @access public + */ + var $_maxFileSize = 1048576; // 1 Mb = 1048576 + + /** + * Flag to know if all fields are frozen + * @since 1.0 + * @var boolean + * @access private + */ + var $_freezeAll = false; + + /** + * Array containing the form rules + * @since 1.0 + * @var array + * @access private + */ + var $_rules = array(); + + /** + * Form rules, global variety + * @var array + * @access private + */ + var $_formRules = array(); + + /** + * Array containing the validation errors + * @since 1.0 + * @var array + * @access private + */ + var $_errors = array(); + + /** + * Note for required fields in the form + * @var string + * @since 1.0 + * @access private + */ + var $_requiredNote = '* denotes required field'; + + /** + * Whether the form was submitted + * @var boolean + * @access private + */ + var $_flagSubmitted = false; + + // }}} + // {{{ constructor + + /** + * Class constructor + * @param string $formName Form's name. + * @param string $method (optional)Form's method defaults to 'POST' + * @param string $action (optional)Form's action + * @param string $target (optional)Form's target defaults to '_self' + * @param mixed $attributes (optional)Extra attributes for
tag + * @param bool $trackSubmit (optional)Whether to track if the form was submitted by adding a special hidden field + * @access public + */ + function HTML_QuickForm($formName='', $method='post', $action='', $target='', $attributes=null, $trackSubmit = false) + { + HTML_Common::HTML_Common($attributes); + $method = (strtoupper($method) == 'GET') ? 'get' : 'post'; + $action = ($action == '') ? $_SERVER['PHP_SELF'] : $action; + $target = empty($target) ? array() : array('target' => $target); + $attributes = array('action'=>$action, 'method'=>$method, 'name'=>$formName, 'id'=>$formName) + $target; + $this->updateAttributes($attributes); + if (!$trackSubmit || isset($_REQUEST['_qf__' . $formName])) { + if (1 == get_magic_quotes_gpc()) { + $this->_submitValues = $this->_recursiveFilter('stripslashes', 'get' == $method? $_GET: $_POST); + foreach ($_FILES as $keyFirst => $valFirst) { + foreach ($valFirst as $keySecond => $valSecond) { + if ('name' == $keySecond) { + $this->_submitFiles[$keyFirst][$keySecond] = $this->_recursiveFilter('stripslashes', $valSecond); + } else { + $this->_submitFiles[$keyFirst][$keySecond] = $valSecond; + } + } + } + } else { + $this->_submitValues = 'get' == $method? $_GET: $_POST; + $this->_submitFiles = $_FILES; + } + $this->_flagSubmitted = count($this->_submitValues) > 0 || count($this->_submitFiles) > 0; + } + if ($trackSubmit) { + unset($this->_submitValues['_qf__' . $formName]); + $this->addElement('hidden', '_qf__' . $formName, null); + } + if (preg_match('/^([0-9]+)([a-zA-Z]*)$/', ini_get('upload_max_filesize'), $matches)) { + // see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + switch (strtoupper($matches['2'])) { + case 'G': + $this->_maxFileSize = $matches['1'] * 1073741824; + break; + case 'M': + $this->_maxFileSize = $matches['1'] * 1048576; + break; + case 'K': + $this->_maxFileSize = $matches['1'] * 1024; + break; + default: + $this->_maxFileSize = $matches['1']; + } + } + } // end constructor + + // }}} + // {{{ apiVersion() + + /** + * Returns the current API version + * + * @since 1.0 + * @access public + * @return float + */ + function apiVersion() + { + return 3.2; + } // end func apiVersion + + // }}} + // {{{ registerElementType() + + /** + * Registers a new element type + * + * @param string $typeName Name of element type + * @param string $include Include path for element type + * @param string $className Element class name + * @since 1.0 + * @access public + * @return void + */ + function registerElementType($typeName, $include, $className) + { + $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'][strtolower($typeName)] = array($include, $className); + } // end func registerElementType + + // }}} + // {{{ registerRule() + + /** + * Registers a new validation rule + * + * @param string $ruleName Name of validation rule + * @param string $type Either: 'regex', 'function' or 'rule' for an HTML_QuickForm_Rule object + * @param string $data1 Name of function, regular expression or HTML_QuickForm_Rule classname + * @param string $data2 Object parent of above function or HTML_QuickForm_Rule file path + * @since 1.0 + * @access public + * @return void + */ + function registerRule($ruleName, $type, $data1, $data2 = null) + { + include_once('HTML/QuickForm/RuleRegistry.php'); + $registry =& HTML_QuickForm_RuleRegistry::singleton(); + $registry->registerRule($ruleName, $type, $data1, $data2); + } // end func registerRule + + // }}} + // {{{ elementExists() + + /** + * Returns true if element is in the form + * + * @param string $element form name of element to check + * @since 1.0 + * @access public + * @return boolean + */ + function elementExists($element=null) + { + return isset($this->_elementIndex[$element]); + } // end func elementExists + + // }}} + // {{{ setDatasource() + + /** + * Sets a datasource object for this form object + * + * Datasource default and constant values will feed the QuickForm object if + * the datasource implements defaultValues() and constantValues() methods. + * + * @param object $datasource datasource object implementing the informal datasource protocol + * @param mixed $defaultsFilter string or array of filter(s) to apply to default values + * @param mixed $constantsFilter string or array of filter(s) to apply to constants values + * @since 3.3 + * @access public + * @return void + * @throws HTML_QuickForm_Error + */ + function setDatasource(&$datasource, $defaultsFilter = null, $constantsFilter = null) + { + if (is_object($datasource)) { + $this->_datasource =& $datasource; + if (is_callable(array($datasource, 'defaultValues'))) { + $this->setDefaults($datasource->defaultValues($this), $defaultsFilter); + } + if (is_callable(array($datasource, 'constantValues'))) { + $this->setConstants($datasource->constantValues($this), $constantsFilter); + } + } else { + return PEAR::raiseError(null, QUICKFORM_INVALID_DATASOURCE, null, E_USER_WARNING, "Datasource is not an object in QuickForm::setDatasource()", 'HTML_QuickForm_Error', true); + } + } // end func setDatasource + + // }}} + // {{{ setDefaults() + + /** + * Initializes default form values + * + * @param array $defaultValues values used to fill the form + * @param mixed $filter (optional) filter(s) to apply to all default values + * @since 1.0 + * @access public + * @return void + * @throws HTML_QuickForm_Error + */ + function setDefaults($defaultValues = null, $filter = null) + { + if (is_array($defaultValues)) { + if (isset($filter)) { + if (is_array($filter) && (2 != count($filter) || !is_callable($filter))) { + foreach ($filter as $val) { + if (!is_callable($val)) { + return PEAR::raiseError(null, QUICKFORM_INVALID_FILTER, null, E_USER_WARNING, "Callback function does not exist in QuickForm::setDefaults()", 'HTML_QuickForm_Error', true); + } else { + $defaultValues = $this->_recursiveFilter($val, $defaultValues); + } + } + } elseif (!is_callable($filter)) { + return PEAR::raiseError(null, QUICKFORM_INVALID_FILTER, null, E_USER_WARNING, "Callback function does not exist in QuickForm::setDefaults()", 'HTML_QuickForm_Error', true); + } else { + $defaultValues = $this->_recursiveFilter($filter, $defaultValues); + } + } + $this->_defaultValues = HTML_QuickForm::arrayMerge($this->_defaultValues, $defaultValues); + foreach (array_keys($this->_elements) as $key) { + $this->_elements[$key]->onQuickFormEvent('updateValue', null, $this); + } + } + } // end func setDefaults + + // }}} + // {{{ setConstants() + + /** + * Initializes constant form values. + * These values won't get overridden by POST or GET vars + * + * @param array $constantValues values used to fill the form + * @param mixed $filter (optional) filter(s) to apply to all default values + * + * @since 2.0 + * @access public + * @return void + * @throws HTML_QuickForm_Error + */ + function setConstants($constantValues = null, $filter = null) + { + if (is_array($constantValues)) { + if (isset($filter)) { + if (is_array($filter) && (2 != count($filter) || !is_callable($filter))) { + foreach ($filter as $val) { + if (!is_callable($val)) { + return PEAR::raiseError(null, QUICKFORM_INVALID_FILTER, null, E_USER_WARNING, "Callback function does not exist in QuickForm::setConstants()", 'HTML_QuickForm_Error', true); + } else { + $constantValues = $this->_recursiveFilter($val, $constantValues); + } + } + } elseif (!is_callable($filter)) { + return PEAR::raiseError(null, QUICKFORM_INVALID_FILTER, null, E_USER_WARNING, "Callback function does not exist in QuickForm::setConstants()", 'HTML_QuickForm_Error', true); + } else { + $constantValues = $this->_recursiveFilter($filter, $constantValues); + } + } + $this->_constantValues = HTML_QuickForm::arrayMerge($this->_constantValues, $constantValues); + foreach (array_keys($this->_elements) as $key) { + $this->_elements[$key]->onQuickFormEvent('updateValue', null, $this); + } + } + } // end func setConstants + + // }}} + // {{{ setMaxFileSize() + + /** + * Sets the value of MAX_FILE_SIZE hidden element + * + * @param int $bytes Size in bytes + * @since 3.0 + * @access public + * @return void + */ + function setMaxFileSize($bytes = 0) + { + if ($bytes > 0) { + $this->_maxFileSize = $bytes; + } + if (!$this->elementExists('MAX_FILE_SIZE')) { + $this->addElement('hidden', 'MAX_FILE_SIZE', $this->_maxFileSize); + } else { + $el =& $this->getElement('MAX_FILE_SIZE'); + $el->updateAttributes(array('value' => $this->_maxFileSize)); + } + } // end func setMaxFileSize + + // }}} + // {{{ getMaxFileSize() + + /** + * Returns the value of MAX_FILE_SIZE hidden element + * + * @since 3.0 + * @access public + * @return int max file size in bytes + */ + function getMaxFileSize() + { + return $this->_maxFileSize; + } // end func getMaxFileSize + + // }}} + // {{{ &createElement() + + /** + * Creates a new form element of the given type. + * + * This method accepts variable number of parameters, their + * meaning and count depending on $elementType + * + * @param string $elementType type of element to add (text, textarea, file...) + * @since 1.0 + * @access public + * @return HTML_QuickForm_Element + * @throws HTML_QuickForm_Error + */ + function &createElement($elementType) + { + $args = func_get_args(); + $element =& HTML_QuickForm::_loadElement('createElement', $elementType, array_slice($args, 1)); + return $element; + } // end func createElement + + // }}} + // {{{ _loadElement() + + /** + * Returns a form element of the given type + * + * @param string $event event to send to newly created element ('createElement' or 'addElement') + * @param string $type element type + * @param array $args arguments for event + * @since 2.0 + * @access private + * @return HTML_QuickForm_Element + * @throws HTML_QuickForm_Error + */ + function &_loadElement($event, $type, $args) + { + $type = strtolower($type); + if (!HTML_QuickForm::isTypeRegistered($type)) { + $error = PEAR::raiseError(null, QUICKFORM_UNREGISTERED_ELEMENT, null, E_USER_WARNING, "Element '$type' does not exist in HTML_QuickForm::_loadElement()", 'HTML_QuickForm_Error', true); + return $error; + } + $className = $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'][$type][1]; + $includeFile = $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'][$type][0]; + include_once($includeFile); + $elementObject =& new $className(); + for ($i = 0; $i < 5; $i++) { + if (!isset($args[$i])) { + $args[$i] = null; + } + } + $err = $elementObject->onQuickFormEvent($event, $args, $this); + if ($err !== true) { + return $err; + } + return $elementObject; + } // end func _loadElement + + // }}} + // {{{ addElement() + + /** + * Adds an element into the form + * + * If $element is a string representing element type, then this + * method accepts variable number of parameters, their meaning + * and count depending on $element + * + * @param mixed $element element object or type of element to add (text, textarea, file...) + * @since 1.0 + * @return HTML_QuickForm_Element a reference to newly added element + * @access public + * @throws HTML_QuickForm_Error + */ + function &addElement($element) + { + if (is_object($element) && is_subclass_of($element, 'html_quickform_element')) { + $elementObject = &$element; + $elementObject->onQuickFormEvent('updateValue', null, $this); + } else { + $args = func_get_args(); + $elementObject =& $this->_loadElement('addElement', $element, array_slice($args, 1)); + if (PEAR::isError($elementObject)) { + return $elementObject; + } + } + $elementName = $elementObject->getName(); + + // Add the element if it is not an incompatible duplicate + if (!empty($elementName) && isset($this->_elementIndex[$elementName])) { + if ($this->_elements[$this->_elementIndex[$elementName]]->getType() == + $elementObject->getType()) { + $this->_elements[] =& $elementObject; + $elKeys = array_keys($this->_elements); + $this->_duplicateIndex[$elementName][] = end($elKeys); + } else { + $error = PEAR::raiseError(null, QUICKFORM_INVALID_ELEMENT_NAME, null, E_USER_WARNING, "Element '$elementName' already exists in HTML_QuickForm::addElement()", 'HTML_QuickForm_Error', true); + return $error; + } + } else { + $this->_elements[] =& $elementObject; + $elKeys = array_keys($this->_elements); + $this->_elementIndex[$elementName] = end($elKeys); + } + if ($this->_freezeAll) { + $elementObject->freeze(); + } + + return $elementObject; + } // end func addElement + + // }}} + // {{{ insertElementBefore() + + /** + * Inserts a new element right before the other element + * + * Warning: it is not possible to check whether the $element is already + * added to the form, therefore if you want to move the existing form + * element to a new position, you'll have to use removeElement(): + * $form->insertElementBefore($form->removeElement('foo', false), 'bar'); + * + * @access public + * @since 3.2.4 + * @param HTML_QuickForm_element Element to insert + * @param string Name of the element before which the new + * one is inserted + * @return HTML_QuickForm_element reference to inserted element + * @throws HTML_QuickForm_Error + */ + function &insertElementBefore(&$element, $nameAfter) + { + if (!empty($this->_duplicateIndex[$nameAfter])) { + $error = PEAR::raiseError(null, QUICKFORM_INVALID_ELEMENT_NAME, null, E_USER_WARNING, 'Several elements named "' . $nameAfter . '" exist in HTML_QuickForm::insertElementBefore().', 'HTML_QuickForm_Error', true); + return $error; + } elseif (!$this->elementExists($nameAfter)) { + $error = PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$nameAfter' does not exist in HTML_QuickForm::insertElementBefore()", 'HTML_QuickForm_Error', true); + return $error; + } + $elementName = $element->getName(); + $targetIdx = $this->_elementIndex[$nameAfter]; + $duplicate = false; + // Like in addElement(), check that it's not an incompatible duplicate + if (!empty($elementName) && isset($this->_elementIndex[$elementName])) { + if ($this->_elements[$this->_elementIndex[$elementName]]->getType() != $element->getType()) { + $error = PEAR::raiseError(null, QUICKFORM_INVALID_ELEMENT_NAME, null, E_USER_WARNING, "Element '$elementName' already exists in HTML_QuickForm::insertElementBefore()", 'HTML_QuickForm_Error', true); + return $error; + } + $duplicate = true; + } + // Move all the elements after added back one place, reindex _elementIndex and/or _duplicateIndex + $elKeys = array_keys($this->_elements); + for ($i = end($elKeys); $i >= $targetIdx; $i--) { + if (isset($this->_elements[$i])) { + $currentName = $this->_elements[$i]->getName(); + $this->_elements[$i + 1] =& $this->_elements[$i]; + if ($this->_elementIndex[$currentName] == $i) { + $this->_elementIndex[$currentName] = $i + 1; + } else { + $dupIdx = array_search($i, $this->_duplicateIndex[$currentName]); + $this->_duplicateIndex[$currentName][$dupIdx] = $i + 1; + } + unset($this->_elements[$i]); + } + } + // Put the element in place finally + $this->_elements[$targetIdx] =& $element; + if (!$duplicate) { + $this->_elementIndex[$elementName] = $targetIdx; + } else { + $this->_duplicateIndex[$elementName][] = $targetIdx; + } + $element->onQuickFormEvent('updateValue', null, $this); + if ($this->_freezeAll) { + $element->freeze(); + } + // If not done, the elements will appear in reverse order + ksort($this->_elements); + return $element; + } + + // }}} + // {{{ addGroup() + + /** + * Adds an element group + * @param array $elements array of elements composing the group + * @param string $name (optional)group name + * @param string $groupLabel (optional)group label + * @param string $separator (optional)string to separate elements + * @param string $appendName (optional)specify whether the group name should be + * used in the form element name ex: group[element] + * @return HTML_QuickForm_group reference to a newly added group + * @since 2.8 + * @access public + * @throws HTML_QuickForm_Error + */ + function &addGroup($elements, $name=null, $groupLabel='', $separator=null, $appendName = true) + { + static $anonGroups = 1; + + if (0 == strlen($name)) { + $name = 'qf_group_' . $anonGroups++; + $appendName = false; + } + $group =& $this->addElement('group', $name, $groupLabel, $elements, $separator, $appendName); + return $group; + } // end func addGroup + + // }}} + // {{{ &getElement() + + /** + * Returns a reference to the element + * + * @param string $element Element name + * @since 2.0 + * @access public + * @return HTML_QuickForm_element reference to element + * @throws HTML_QuickForm_Error + */ + function &getElement($element) + { + if (isset($this->_elementIndex[$element])) { + return $this->_elements[$this->_elementIndex[$element]]; + } else { + $error = PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$element' does not exist in HTML_QuickForm::getElement()", 'HTML_QuickForm_Error', true); + return $error; + } + } // end func getElement + + // }}} + // {{{ &getElementValue() + + /** + * Returns the element's raw value + * + * This returns the value as submitted by the form (not filtered) + * or set via setDefaults() or setConstants() + * + * @param string $element Element name + * @since 2.0 + * @access public + * @return mixed element value + * @throws HTML_QuickForm_Error + */ + function &getElementValue($element) + { + if (!isset($this->_elementIndex[$element])) { + $error = PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$element' does not exist in HTML_QuickForm::getElementValue()", 'HTML_QuickForm_Error', true); + return $error; + } + $value = $this->_elements[$this->_elementIndex[$element]]->getValue(); + if (isset($this->_duplicateIndex[$element])) { + foreach ($this->_duplicateIndex[$element] as $index) { + if (null !== ($v = $this->_elements[$index]->getValue())) { + if (is_array($value)) { + $value[] = $v; + } else { + $value = (null === $value)? $v: array($value, $v); + } + } + } + } + return $value; + } // end func getElementValue + + // }}} + // {{{ getSubmitValue() + + /** + * Returns the elements value after submit and filter + * + * @param string Element name + * @since 2.0 + * @access public + * @return mixed submitted element value or null if not set + */ + function getSubmitValue($elementName) + { + $value = null; + if (isset($this->_submitValues[$elementName]) || isset($this->_submitFiles[$elementName])) { + $value = isset($this->_submitValues[$elementName])? $this->_submitValues[$elementName]: array(); + if (is_array($value) && isset($this->_submitFiles[$elementName])) { + foreach ($this->_submitFiles[$elementName] as $k => $v) { + $value = HTML_QuickForm::arrayMerge($value, $this->_reindexFiles($this->_submitFiles[$elementName][$k], $k)); + } + } + + } elseif ('file' == $this->getElementType($elementName)) { + return $this->getElementValue($elementName); + + } elseif (false !== ($pos = strpos($elementName, '['))) { + $base = str_replace( + array('\\', '\''), array('\\\\', '\\\''), + substr($elementName, 0, $pos) + ); + $idx = "['" . str_replace( + array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"), + substr($elementName, $pos + 1, -1) + ) . "']"; + if (isset($this->_submitValues[$base])) { + $value = eval("return (isset(\$this->_submitValues['{$base}']{$idx})) ? \$this->_submitValues['{$base}']{$idx} : null;"); + } + + if ((is_array($value) || null === $value) && isset($this->_submitFiles[$base])) { + $props = array('name', 'type', 'size', 'tmp_name', 'error'); + $code = "if (!isset(\$this->_submitFiles['{$base}']['name']{$idx})) {\n" . + " return null;\n" . + "} else {\n" . + " \$v = array();\n"; + foreach ($props as $prop) { + $code .= " \$v = HTML_QuickForm::arrayMerge(\$v, \$this->_reindexFiles(\$this->_submitFiles['{$base}']['{$prop}']{$idx}, '{$prop}'));\n"; + } + $fileValue = eval($code . " return \$v;\n}\n"); + if (null !== $fileValue) { + $value = null === $value? $fileValue: HTML_QuickForm::arrayMerge($value, $fileValue); + } + } + } + + // This is only supposed to work for groups with appendName = false + if (null === $value && 'group' == $this->getElementType($elementName)) { + $group =& $this->getElement($elementName); + $elements =& $group->getElements(); + foreach (array_keys($elements) as $key) { + $name = $group->getElementName($key); + // prevent endless recursion in case of radios and such + if ($name != $elementName) { + if (null !== ($v = $this->getSubmitValue($name))) { + $value[$name] = $v; + } + } + } + } + return $value; + } // end func getSubmitValue + + // }}} + // {{{ _reindexFiles() + + /** + * A helper function to change the indexes in $_FILES array + * + * @param mixed Some value from the $_FILES array + * @param string The key from the $_FILES array that should be appended + * @return array + */ + function _reindexFiles($value, $key) + { + if (!is_array($value)) { + return array($key => $value); + } else { + $ret = array(); + foreach ($value as $k => $v) { + $ret[$k] = $this->_reindexFiles($v, $key); + } + return $ret; + } + } + + // }}} + // {{{ getElementError() + + /** + * Returns error corresponding to validated element + * + * @param string $element Name of form element to check + * @since 1.0 + * @access public + * @return string error message corresponding to checked element + */ + function getElementError($element) + { + if (isset($this->_errors[$element])) { + return $this->_errors[$element]; + } + } // end func getElementError + + // }}} + // {{{ setElementError() + + /** + * Set error message for a form element + * + * @param string $element Name of form element to set error for + * @param string $message Error message, if empty then removes the current error message + * @since 1.0 + * @access public + * @return void + */ + function setElementError($element, $message = null) + { + if (!empty($message)) { + $this->_errors[$element] = $message; + } else { + unset($this->_errors[$element]); + } + } // end func setElementError + + // }}} + // {{{ getElementType() + + /** + * Returns the type of the given element + * + * @param string $element Name of form element + * @since 1.1 + * @access public + * @return string Type of the element, false if the element is not found + */ + function getElementType($element) + { + if (isset($this->_elementIndex[$element])) { + return $this->_elements[$this->_elementIndex[$element]]->getType(); + } + return false; + } // end func getElementType + + // }}} + // {{{ updateElementAttr() + + /** + * Updates Attributes for one or more elements + * + * @param mixed $elements Array of element names/objects or string of elements to be updated + * @param mixed $attrs Array or sting of html attributes + * @since 2.10 + * @access public + * @return void + */ + function updateElementAttr($elements, $attrs) + { + if (is_string($elements)) { + $elements = split('[ ]?,[ ]?', $elements); + } + foreach (array_keys($elements) as $key) { + if (is_object($elements[$key]) && is_a($elements[$key], 'HTML_QuickForm_element')) { + $elements[$key]->updateAttributes($attrs); + } elseif (isset($this->_elementIndex[$elements[$key]])) { + $this->_elements[$this->_elementIndex[$elements[$key]]]->updateAttributes($attrs); + if (isset($this->_duplicateIndex[$elements[$key]])) { + foreach ($this->_duplicateIndex[$elements[$key]] as $index) { + $this->_elements[$index]->updateAttributes($attrs); + } + } + } + } + } // end func updateElementAttr + + // }}} + // {{{ removeElement() + + /** + * Removes an element + * + * The method "unlinks" an element from the form, returning the reference + * to the element object. If several elements named $elementName exist, + * it removes the first one, leaving the others intact. + * + * @param string $elementName The element name + * @param boolean $removeRules True if rules for this element are to be removed too + * @access public + * @since 2.0 + * @return HTML_QuickForm_element a reference to the removed element + * @throws HTML_QuickForm_Error + */ + function &removeElement($elementName, $removeRules = true) + { + if (!isset($this->_elementIndex[$elementName])) { + $error = PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$elementName' does not exist in HTML_QuickForm::removeElement()", 'HTML_QuickForm_Error', true); + return $error; + } + $el =& $this->_elements[$this->_elementIndex[$elementName]]; + unset($this->_elements[$this->_elementIndex[$elementName]]); + if (empty($this->_duplicateIndex[$elementName])) { + unset($this->_elementIndex[$elementName]); + } else { + $this->_elementIndex[$elementName] = array_shift($this->_duplicateIndex[$elementName]); + } + if ($removeRules) { + unset($this->_rules[$elementName], $this->_errors[$elementName]); + } + return $el; + } // end func removeElement + + // }}} + // {{{ addRule() + + /** + * Adds a validation rule for the given field + * + * If the element is in fact a group, it will be considered as a whole. + * To validate grouped elements as separated entities, + * use addGroupRule instead of addRule. + * + * @param string $element Form element name + * @param string $message Message to display for invalid data + * @param string $type Rule type, use getRegisteredRules() to get types + * @param string $format (optional)Required for extra rule data + * @param string $validation (optional)Where to perform validation: "server", "client" + * @param boolean $reset Client-side validation: reset the form element to its original value if there is an error? + * @param boolean $force Force the rule to be applied, even if the target form element does not exist + * @since 1.0 + * @access public + * @throws HTML_QuickForm_Error + */ + function addRule($element, $message, $type, $format=null, $validation='server', $reset = false, $force = false) + { + if (!$force) { + if (!is_array($element) && !$this->elementExists($element)) { + return PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$element' does not exist in HTML_QuickForm::addRule()", 'HTML_QuickForm_Error', true); + } elseif (is_array($element)) { + foreach ($element as $el) { + if (!$this->elementExists($el)) { + return PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Element '$el' does not exist in HTML_QuickForm::addRule()", 'HTML_QuickForm_Error', true); + } + } + } + } + if (false === ($newName = $this->isRuleRegistered($type, true))) { + return PEAR::raiseError(null, QUICKFORM_INVALID_RULE, null, E_USER_WARNING, "Rule '$type' is not registered in HTML_QuickForm::addRule()", 'HTML_QuickForm_Error', true); + } elseif (is_string($newName)) { + $type = $newName; + } + if (is_array($element)) { + $dependent = $element; + $element = array_shift($dependent); + } else { + $dependent = null; + } + if ($type == 'required' || $type == 'uploadedfile') { + $this->_required[] = $element; + } + if (!isset($this->_rules[$element])) { + $this->_rules[$element] = array(); + } + if ($validation == 'client') { + $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);')); + } + $this->_rules[$element][] = array( + 'type' => $type, + 'format' => $format, + 'message' => $message, + 'validation' => $validation, + 'reset' => $reset, + 'dependent' => $dependent + ); + } // end func addRule + + // }}} + // {{{ addGroupRule() + + /** + * Adds a validation rule for the given group of elements + * + * Only groups with a name can be assigned a validation rule + * Use addGroupRule when you need to validate elements inside the group. + * Use addRule if you need to validate the group as a whole. In this case, + * the same rule will be applied to all elements in the group. + * Use addRule if you need to validate the group against a function. + * + * @param string $group Form group name + * @param mixed $arg1 Array for multiple elements or error message string for one element + * @param string $type (optional)Rule type use getRegisteredRules() to get types + * @param string $format (optional)Required for extra rule data + * @param int $howmany (optional)How many valid elements should be in the group + * @param string $validation (optional)Where to perform validation: "server", "client" + * @param bool $reset Client-side: whether to reset the element's value to its original state if validation failed. + * @since 2.5 + * @access public + * @throws HTML_QuickForm_Error + */ + function addGroupRule($group, $arg1, $type='', $format=null, $howmany=0, $validation = 'server', $reset = false) + { + if (!$this->elementExists($group)) { + return PEAR::raiseError(null, QUICKFORM_NONEXIST_ELEMENT, null, E_USER_WARNING, "Group '$group' does not exist in HTML_QuickForm::addGroupRule()", 'HTML_QuickForm_Error', true); + } + + $groupObj =& $this->getElement($group); + if (is_array($arg1)) { + $required = 0; + foreach ($arg1 as $elementIndex => $rules) { + $elementName = $groupObj->getElementName($elementIndex); + foreach ($rules as $rule) { + $format = (isset($rule[2])) ? $rule[2] : null; + $validation = (isset($rule[3]) && 'client' == $rule[3])? 'client': 'server'; + $reset = isset($rule[4]) && $rule[4]; + $type = $rule[1]; + if (false === ($newName = $this->isRuleRegistered($type, true))) { + return PEAR::raiseError(null, QUICKFORM_INVALID_RULE, null, E_USER_WARNING, "Rule '$type' is not registered in HTML_QuickForm::addGroupRule()", 'HTML_QuickForm_Error', true); + } elseif (is_string($newName)) { + $type = $newName; + } + + $this->_rules[$elementName][] = array( + 'type' => $type, + 'format' => $format, + 'message' => $rule[0], + 'validation' => $validation, + 'reset' => $reset, + 'group' => $group); + + if ('required' == $type || 'uploadedfile' == $type) { + $groupObj->_required[] = $elementName; + $this->_required[] = $elementName; + $required++; + } + if ('client' == $validation) { + $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);')); + } + } + } + if ($required > 0 && count($groupObj->getElements()) == $required) { + $this->_required[] = $group; + } + } elseif (is_string($arg1)) { + if (false === ($newName = $this->isRuleRegistered($type, true))) { + return PEAR::raiseError(null, QUICKFORM_INVALID_RULE, null, E_USER_WARNING, "Rule '$type' is not registered in HTML_QuickForm::addGroupRule()", 'HTML_QuickForm_Error', true); + } elseif (is_string($newName)) { + $type = $newName; + } + + // addGroupRule() should also handle form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for an elements + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_button extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $value (optional)Input field value + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_button($elementName=null, $value=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, null, $attributes); + $this->_persistantFreeze = false; + $this->setValue($value); + $this->setType('button'); + } //end constructor + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + +} //end class HTML_QuickForm_button +?> diff --git a/HTML/QuickForm/checkbox.php b/HTML/QuickForm/checkbox.php new file mode 100644 index 0000000..9ded15c --- /dev/null +++ b/HTML/QuickForm/checkbox.php @@ -0,0 +1,277 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: checkbox.php,v 1.22 2007/06/03 15:25:28 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a checkbox type field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_checkbox extends HTML_QuickForm_input +{ + // {{{ properties + + /** + * Checkbox display text + * @var string + * @since 1.1 + * @access private + */ + var $_text = ''; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $elementLabel (optional)Input field value + * @param string $text (optional)Checkbox display text + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_checkbox($elementName=null, $elementLabel=null, $text='', $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->_text = $text; + $this->setType('checkbox'); + $this->updateAttributes(array('value'=>1)); + $this->_generateId(); + } //end constructor + + // }}} + // {{{ setChecked() + + /** + * Sets whether a checkbox is checked + * + * @param bool $checked Whether the field is checked or not + * @since 1.0 + * @access public + * @return void + */ + function setChecked($checked) + { + if (!$checked) { + $this->removeAttribute('checked'); + } else { + $this->updateAttributes(array('checked'=>'checked')); + } + } //end func setChecked + + // }}} + // {{{ getChecked() + + /** + * Returns whether a checkbox is checked + * + * @since 1.0 + * @access public + * @return bool + */ + function getChecked() + { + return (bool)$this->getAttribute('checked'); + } //end func getChecked + + // }}} + // {{{ toHtml() + + /** + * Returns the checkbox element in HTML + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + if (0 == strlen($this->_text)) { + $label = ''; + } elseif ($this->_flagFrozen) { + $label = $this->_text; + } else { + $label = ''; + } + return HTML_QuickForm_input::toHtml() . $label; + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @since 1.0 + * @access public + * @return string + */ + function getFrozenHtml() + { + if ($this->getChecked()) { + return '[x]' . + $this->_getPersistantData(); + } else { + return '[ ]'; + } + } //end func getFrozenHtml + + // }}} + // {{{ setText() + + /** + * Sets the checkbox text + * + * @param string $text + * @since 1.1 + * @access public + * @return void + */ + function setText($text) + { + $this->_text = $text; + } //end func setText + + // }}} + // {{{ getText() + + /** + * Returns the checkbox text + * + * @since 1.1 + * @access public + * @return string + */ + function getText() + { + return $this->_text; + } //end func getText + + // }}} + // {{{ setValue() + + /** + * Sets the value of the form element + * + * @param string $value Default value of the form element + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + return $this->setChecked($value); + } // end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the form element + * + * @since 1.0 + * @access public + * @return bool + */ + function getValue() + { + return $this->getChecked(); + } // end func getValue + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'updateValue': + // constant values override both default and submitted ones + // default values are overriden by submitted + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + // if no boxes were checked, then there is no value in the array + // yet we don't want to display default value in this case + if ($caller->isSubmitted()) { + $value = $this->_findValue($caller->_submitValues); + } else { + $value = $this->_findValue($caller->_defaultValues); + } + } + if (null !== $value || $caller->isSubmitted()) { + $this->setChecked($value); + } + break; + case 'setGroupValue': + $this->setChecked($arg); + break; + default: + parent::onQuickFormEvent($event, $arg, $caller); + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ exportValue() + + /** + * Return true if the checkbox is checked, null if it is not checked (getValue() returns false) + */ + function exportValue(&$submitValues, $assoc = false) + { + $value = $this->_findValue($submitValues); + if (null === $value) { + $value = $this->getChecked()? true: null; + } + return $this->_prepareValue($value, $assoc); + } + + // }}} +} //end class HTML_QuickForm_checkbox +?> diff --git a/HTML/QuickForm/date.php b/HTML/QuickForm/date.php new file mode 100644 index 0000000..bf3858a --- /dev/null +++ b/HTML/QuickForm/date.php @@ -0,0 +1,511 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: date.php,v 1.60 2007/06/04 19:22:23 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Class for a group of form elements + */ +require_once 'HTML/QuickForm/group.php'; +/** + * Class for elements + */ +require_once 'HTML/QuickForm/select.php'; + +/** + * Class for a group of elements used to input dates (and times). + * + * Inspired by original 'date' element but reimplemented as a subclass + * of HTML_QuickForm_group + * + * @category HTML + * @package HTML_QuickForm + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 3.1 + */ +class HTML_QuickForm_date extends HTML_QuickForm_group +{ + // {{{ properties + + /** + * Various options to control the element's display. + * + * @access private + * @var array + */ + var $_options = array( + 'language' => 'en', + 'format' => 'dMY', + 'minYear' => 2001, + 'maxYear' => 2010, + 'addEmptyOption' => false, + 'emptyOptionValue' => '', + 'emptyOptionText' => ' ', + 'optionIncrement' => array('i' => 1, 's' => 1) + ); + + /** + * These complement separators, they are appended to the resultant HTML + * @access private + * @var array + */ + var $_wrap = array('', ''); + + /** + * Options in different languages + * + * Note to potential translators: to avoid encoding problems please send + * your translations with "weird" letters encoded as HTML Unicode entities + * + * @access private + * @var array + */ + var $_locale = array( + 'en' => array ( + 'weekdays_short'=> array ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'), + 'weekdays_long' => array ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + 'months_long' => array ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December') + ), + 'de' => array ( + 'weekdays_short'=> array ('So', 'Mon', 'Di', 'Mi', 'Do', 'Fr', 'Sa'), + 'weekdays_long' => array ('Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'), + 'months_short' => array ('Jan', 'Feb', 'März', 'April', 'Mai', 'Juni', 'Juli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dez'), + 'months_long' => array ('Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember') + ), + 'fr' => array ( + 'weekdays_short'=> array ('Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'), + 'weekdays_long' => array ('Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'), + 'months_short' => array ('Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc'), + 'months_long' => array ('Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre') + ), + 'hu' => array ( + 'weekdays_short'=> array ('V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'), + 'weekdays_long' => array ('vasárnap', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', 'szombat'), + 'months_short' => array ('jan', 'feb', 'márc', 'ápr', 'máj', 'jún', 'júl', 'aug', 'szept', 'okt', 'nov', 'dec'), + 'months_long' => array ('január', 'február', 'március', 'április', 'május', 'június', 'július', 'augusztus', 'szeptember', 'október', 'november', 'december') + ), + 'pl' => array ( + 'weekdays_short'=> array ('Nie', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob'), + 'weekdays_long' => array ('Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'), + 'months_short' => array ('Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'), + 'months_long' => array ('Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień') + ), + 'sl' => array ( + 'weekdays_short'=> array ('Ned', 'Pon', 'Tor', 'Sre', 'Cet', 'Pet', 'Sob'), + 'weekdays_long' => array ('Nedelja', 'Ponedeljek', 'Torek', 'Sreda', 'Cetrtek', 'Petek', 'Sobota'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', 'Avgust', 'September', 'Oktober', 'November', 'December') + ), + 'ru' => array ( + 'weekdays_short'=> array ('Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'), + 'weekdays_long' => array ('Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'), + 'months_short' => array ('Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'), + 'months_long' => array ('Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь') + ), + 'es' => array ( + 'weekdays_short'=> array ('Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'), + 'weekdays_long' => array ('Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'), + 'months_short' => array ('Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'), + 'months_long' => array ('Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') + ), + 'da' => array ( + 'weekdays_short'=> array ('Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'), + 'weekdays_long' => array ('Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December') + ), + 'is' => array ( + 'weekdays_short'=> array ('Sun', 'Mán', 'Þri', 'Mið', 'Fim', 'Fös', 'Lau'), + 'weekdays_long' => array ('Sunnudagur', 'Mánudagur', 'Þriðjudagur', 'Miðvikudagur', 'Fimmtudagur', 'Föstudagur', 'Laugardagur'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Maí', 'Jún', 'Júl', 'Ágú', 'Sep', 'Okt', 'Nóv', 'Des'), + 'months_long' => array ('Janúar', 'Febrúar', 'Mars', 'Apríl', 'Maí', 'Júní', 'Júlí', 'Ágúst', 'September', 'Október', 'Nóvember', 'Desember') + ), + 'it' => array ( + 'weekdays_short'=> array ('Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'), + 'weekdays_long' => array ('Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'), + 'months_short' => array ('Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'), + 'months_long' => array ('Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre') + ), + 'sk' => array ( + 'weekdays_short'=> array ('Ned', 'Pon', 'Uto', 'Str', 'Štv', 'Pia', 'Sob'), + 'weekdays_long' => array ('Nedeža', 'Pondelok', 'Utorok', 'Streda', 'Štvrtok', 'Piatok', 'Sobota'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Máj', 'Jún', 'Júl', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Január', 'Február', 'Marec', 'Apríl', 'Máj', 'Jún', 'Júl', 'August', 'September', 'Október', 'November', 'December') + ), + 'cs' => array ( + 'weekdays_short'=> array ('Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'), + 'weekdays_long' => array ('Neděle', 'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota'), + 'months_short' => array ('Led', 'Úno', 'Bře', 'Dub', 'Kvě', 'Čen', 'Čec', 'Srp', 'Zář', 'Říj', 'Lis', 'Pro'), + 'months_long' => array ('Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec') + ), + 'hy' => array ( + 'weekdays_short'=> array ('Կրկ', 'Երկ', 'Երք', 'Չրք', 'Հնգ', 'Ուր', 'Շբթ'), + 'weekdays_long' => array ('Կիրակի', 'Երկուշաբթի', 'Երեքշաբթի', 'Չորեքշաբթի', 'Հինգշաբթի', 'Ուրբաթ', 'Շաբաթ'), + 'months_short' => array ('Հնվ', 'Փտր', 'Մրտ', 'Ապր', 'Մյս', 'Հնս', 'Հլս', 'Օգս', 'Սպտ', 'Հկտ', 'Նյմ', 'Դկտ'), + 'months_long' => array ('Հունվար', 'Փետրվար', 'Մարտ', 'Ապրիլ', 'Մայիս', 'Հունիս', 'Հուլիս', 'Օգոստոս', 'Սեպտեմբեր', 'Հոկտեմբեր', 'Նոյեմբեր', 'Դեկտեմբեր') + ), + 'nl' => array ( + 'weekdays_short'=> array ('Zo', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za'), + 'weekdays_long' => array ('Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December') + ), + 'et' => array ( + 'weekdays_short'=> array ('P', 'E', 'T', 'K', 'N', 'R', 'L'), + 'weekdays_long' => array ('Pühapäev', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev'), + 'months_short' => array ('Jaan', 'Veebr', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'), + 'months_long' => array ('Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', 'August', 'September', 'Oktoober', 'November', 'Detsember') + ), + 'tr' => array ( + 'weekdays_short'=> array ('Paz', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cts'), + 'weekdays_long' => array ('Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi'), + 'months_short' => array ('Ock', 'Şbt', 'Mrt', 'Nsn', 'Mys', 'Hzrn', 'Tmmz', 'Ağst', 'Eyl', 'Ekm', 'Ksm', 'Arlk'), + 'months_long' => array ('Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık') + ), + 'no' => array ( + 'weekdays_short'=> array ('Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'), + 'weekdays_long' => array ('Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'), + 'months_long' => array ('Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Desember') + ), + 'eo' => array ( + 'weekdays_short'=> array ('Dim', 'Lun', 'Mar', 'Mer', 'Ĵaŭ', 'Ven', 'Sab'), + 'weekdays_long' => array ('Dimanĉo', 'Lundo', 'Mardo', 'Merkredo', 'Ĵaŭdo', 'Vendredo', 'Sabato'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aŭg', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Januaro', 'Februaro', 'Marto', 'Aprilo', 'Majo', 'Junio', 'Julio', 'Aŭgusto', 'Septembro', 'Oktobro', 'Novembro', 'Decembro') + ), + 'ua' => array ( + 'weekdays_short'=> array('Ндл', 'Пнд', 'Втр', 'Срд', 'Чтв', 'Птн', 'Сбт'), + 'weekdays_long' => array('Неділя', 'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П\'ятниця', 'Субота'), + 'months_short' => array('Січ', 'Лют', 'Бер', 'Кві', 'Тра', 'Чер', 'Лип', 'Сер', 'Вер', 'Жов', 'Лис', 'Гру'), + 'months_long' => array('Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень') + ), + 'ro' => array ( + 'weekdays_short'=> array ('Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sam'), + 'weekdays_long' => array ('Duminica', 'Luni', 'Marti', 'Miercuri', 'Joi', 'Vineri', 'Sambata'), + 'months_short' => array ('Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + 'months_long' => array ('Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie') + ), + 'he' => array ( + 'weekdays_short'=> array ('ראשון', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת'), + 'weekdays_long' => array ('יום ראשון', 'יום שני', 'יום שלישי', 'יום רביעי', 'יום חמישי', 'יום שישי', 'שבת'), + 'months_short' => array ('ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'), + 'months_long' => array ('ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר') + ), + 'sv' => array ( + 'weekdays_short'=> array ('Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör'), + 'weekdays_long' => array ('Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag'), + 'months_short' => array ('Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'), + 'months_long' => array ('Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December') + ), + 'pt' => array ( + 'weekdays_short'=> array ('Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'), + 'weekdays_long' => array ('Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado'), + 'months_short' => array ('Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'), + 'months_long' => array ('Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro') + ), + 'tw' => array ( + 'weekdays_short'=> array ('週日','週一', '週二','週三', '週四','週五', '週六'), + 'weekdays_long' => array ('星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'), + 'months_short' => array ('一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'), + 'months_long' => array ('一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月') + ), + 'pt-br' => array ( + 'weekdays_short'=> array ('Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'), + 'weekdays_long' => array ('Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'), + 'months_short' => array ('Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'), + 'months_long' => array ('Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro') + ) + ); + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * The following keys may appear in $options array: + * - 'language': date language + * - 'format': Format of the date, based on PHP's date() function. + * The following characters are currently recognised in format string: + *
  
+    *       D => Short names of days
+    *       l => Long names of days
+    *       d => Day numbers
+    *       M => Short names of months
+    *       F => Long names of months
+    *       m => Month numbers
+    *       Y => Four digit year
+    *       y => Two digit year
+    *       h => 12 hour format
+    *       H => 23 hour  format
+    *       i => Minutes
+    *       s => Seconds
+    *       a => am/pm
+    *       A => AM/PM
+    *   
+ * - 'minYear': Minimum year in year select + * - 'maxYear': Maximum year in year select + * - 'addEmptyOption': Should an empty option be added to the top of + * each select box? + * - 'emptyOptionValue': The value passed by the empty option. + * - 'emptyOptionText': The text displayed for the empty option. + * - 'optionIncrement': Step to increase the option values by (works for 'i' and 's') + * + * @access public + * @param string Element's name + * @param mixed Label(s) for an element + * @param array Options to control the element's display + * @param mixed Either a typical HTML attribute string or an associative array + */ + function HTML_QuickForm_date($elementName = null, $elementLabel = null, $options = array(), $attributes = null) + { + $this->HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->_appendName = true; + $this->_type = 'date'; + // set the options, do not bother setting bogus ones + if (is_array($options)) { + foreach ($options as $name => $value) { + if ('language' == $name) { + $this->_options['language'] = isset($this->_locale[$value])? $value: 'en'; + } elseif (isset($this->_options[$name])) { + if (is_array($value) && is_array($this->_options[$name])) { + $this->_options[$name] = @array_merge($this->_options[$name], $value); + } else { + $this->_options[$name] = $value; + } + } + } + } + } + + // }}} + // {{{ _createElements() + + function _createElements() + { + $this->_separator = $this->_elements = array(); + $separator = ''; + $locale =& $this->_locale[$this->_options['language']]; + $backslash = false; + for ($i = 0, $length = strlen($this->_options['format']); $i < $length; $i++) { + $sign = $this->_options['format']{$i}; + if ($backslash) { + $backslash = false; + $separator .= $sign; + } else { + $loadSelect = true; + switch ($sign) { + case 'D': + // Sunday is 0 like with 'w' in date() + $options = $locale['weekdays_short']; + break; + case 'l': + $options = $locale['weekdays_long']; + break; + case 'd': + $options = $this->_createOptionList(1, 31); + break; + case 'M': + $options = $locale['months_short']; + array_unshift($options , ''); + unset($options[0]); + break; + case 'm': + $options = $this->_createOptionList(1, 12); + break; + case 'F': + $options = $locale['months_long']; + array_unshift($options , ''); + unset($options[0]); + break; + case 'Y': + $options = $this->_createOptionList( + $this->_options['minYear'], + $this->_options['maxYear'], + $this->_options['minYear'] > $this->_options['maxYear']? -1: 1 + ); + break; + case 'y': + $options = $this->_createOptionList( + $this->_options['minYear'], + $this->_options['maxYear'], + $this->_options['minYear'] > $this->_options['maxYear']? -1: 1 + ); + array_walk($options, create_function('&$v,$k','$v = substr($v,-2);')); + break; + case 'h': + $options = $this->_createOptionList(1, 12); + break; + case 'g': + $options = $this->_createOptionList(1, 12); + array_walk($options, create_function('&$v,$k', '$v = intval($v);')); + break; + case 'H': + $options = $this->_createOptionList(0, 23); + break; + case 'i': + $options = $this->_createOptionList(0, 59, $this->_options['optionIncrement']['i']); + break; + case 's': + $options = $this->_createOptionList(0, 59, $this->_options['optionIncrement']['s']); + break; + case 'a': + $options = array('am' => 'am', 'pm' => 'pm'); + break; + case 'A': + $options = array('AM' => 'AM', 'PM' => 'PM'); + break; + case 'W': + $options = $this->_createOptionList(1, 53); + break; + case '\\': + $backslash = true; + $loadSelect = false; + break; + default: + $separator .= (' ' == $sign? ' ': $sign); + $loadSelect = false; + } + + if ($loadSelect) { + if (0 < count($this->_elements)) { + $this->_separator[] = $separator; + } else { + $this->_wrap[0] = $separator; + } + $separator = ''; + // Should we add an empty option to the top of the select? + if (!is_array($this->_options['addEmptyOption']) && $this->_options['addEmptyOption'] || + is_array($this->_options['addEmptyOption']) && !empty($this->_options['addEmptyOption'][$sign])) { + + // Using '+' array operator to preserve the keys + if (is_array($this->_options['emptyOptionText']) && !empty($this->_options['emptyOptionText'][$sign])) { + $options = array($this->_options['emptyOptionValue'] => $this->_options['emptyOptionText'][$sign]) + $options; + } else { + $options = array($this->_options['emptyOptionValue'] => $this->_options['emptyOptionText']) + $options; + } + } + $this->_elements[] =& new HTML_QuickForm_select($sign, null, $options, $this->getAttributes()); + } + } + } + $this->_wrap[1] = $separator . ($backslash? '\\': ''); + } + + // }}} + // {{{ _createOptionList() + + /** + * Creates an option list containing the numbers from the start number to the end, inclusive + * + * @param int The start number + * @param int The end number + * @param int Increment by this value + * @access private + * @return array An array of numeric options. + */ + function _createOptionList($start, $end, $step = 1) + { + for ($i = $start, $options = array(); $start > $end? $i >= $end: $i <= $end; $i += $step) { + $options[$i] = sprintf('%02d', $i); + } + return $options; + } + + // }}} + // {{{ setValue() + + function setValue($value) + { + $trimLeadingZeros = create_function('$a', '$b = ltrim($a, \'0\'); return strlen($b)? $b: \'0\';'); + if (empty($value)) { + $value = array(); + } elseif (is_scalar($value)) { + if (!is_numeric($value)) { + $value = strtotime($value); + } + // might be a unix epoch, then we fill all possible values + $arr = explode('-', date('w-j-n-Y-g-G-i-s-a-A-W', (int)$value)); + $value = array( + 'D' => $arr[0], + 'l' => $arr[0], + 'd' => $arr[1], + 'M' => $arr[2], + 'm' => $arr[2], + 'F' => $arr[2], + 'Y' => $arr[3], + 'y' => $arr[3], + 'h' => $arr[4], + 'g' => $arr[4], + 'H' => $arr[5], + 'i' => $trimLeadingZeros($arr[6]), + 's' => $trimLeadingZeros($arr[7]), + 'a' => $arr[8], + 'A' => $arr[9], + 'W' => $trimLeadingZeros($arr[10]) + ); + } else { + $value = array_map($trimLeadingZeros, $value); + } + parent::setValue($value); + } + + // }}} + // {{{ toHtml() + + function toHtml() + { + include_once('HTML/QuickForm/Renderer/Default.php'); + $renderer =& new HTML_QuickForm_Renderer_Default(); + $renderer->setElementTemplate('{element}'); + parent::accept($renderer); + return $this->_wrap[0] . $renderer->toHtml() . $this->_wrap[1]; + } + + // }}} + // {{{ accept() + + function accept(&$renderer, $required = false, $error = null) + { + $renderer->renderElement($this, $required, $error); + } + + // }}} + // {{{ onQuickFormEvent() + + function onQuickFormEvent($event, $arg, &$caller) + { + if ('updateValue' == $event) { + // we need to call setValue(), 'cause the default/constant value + // may be in fact a timestamp, not an array + return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller); + } else { + return parent::onQuickFormEvent($event, $arg, $caller); + } + } + + // }}} +} +?> \ No newline at end of file diff --git a/HTML/QuickForm/element.php b/HTML/QuickForm/element.php new file mode 100644 index 0000000..f2bb6e0 --- /dev/null +++ b/HTML/QuickForm/element.php @@ -0,0 +1,494 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: element.php,v 1.35 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for all HTML classes + */ +require_once 'HTML/Common.php'; + +/** + * Base class for form elements + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 1.0 + * @abstract + */ +class HTML_QuickForm_element extends HTML_Common +{ + // {{{ properties + + /** + * Label of the field + * @var string + * @since 1.3 + * @access private + */ + var $_label = ''; + + /** + * Form element type + * @var string + * @since 1.0 + * @access private + */ + var $_type = ''; + + /** + * Flag to tell if element is frozen + * @var boolean + * @since 1.0 + * @access private + */ + var $_flagFrozen = false; + + /** + * Does the element support persistant data when frozen + * @var boolean + * @since 1.3 + * @access private + */ + var $_persistantFreeze = false; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string Name of the element + * @param mixed Label(s) for the element + * @param mixed Associative array of tag attributes or HTML attributes name="value" pairs + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_element($elementName=null, $elementLabel=null, $attributes=null) + { + HTML_Common::HTML_Common($attributes); + if (isset($elementName)) { + $this->setName($elementName); + } + if (isset($elementLabel)) { + $this->setLabel($elementLabel); + } + } //end constructor + + // }}} + // {{{ apiVersion() + + /** + * Returns the current API version + * + * @since 1.0 + * @access public + * @return float + */ + function apiVersion() + { + return 3.2; + } // end func apiVersion + + // }}} + // {{{ getType() + + /** + * Returns element type + * + * @since 1.0 + * @access public + * @return string + */ + function getType() + { + return $this->_type; + } // end func getType + + // }}} + // {{{ setName() + + /** + * Sets the input field name + * + * @param string $name Input field name attribute + * @since 1.0 + * @access public + * @return void + */ + function setName($name) + { + // interface method + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @since 1.0 + * @access public + * @return string + */ + function getName() + { + // interface method + } //end func getName + + // }}} + // {{{ setValue() + + /** + * Sets the value of the form element + * + * @param string $value Default value of the form element + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + // interface + } // end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the form element + * + * @since 1.0 + * @access public + * @return mixed + */ + function getValue() + { + // interface + return null; + } // end func getValue + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + $this->_flagFrozen = true; + } //end func freeze + + // }}} + // {{{ unfreeze() + + /** + * Unfreezes the element so that it becomes editable + * + * @access public + * @return void + * @since 3.2.4 + */ + function unfreeze() + { + $this->_flagFrozen = false; + } + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @since 1.0 + * @access public + * @return string + */ + function getFrozenHtml() + { + $value = $this->getValue(); + return ('' != $value? htmlspecialchars($value): ' ') . + $this->_getPersistantData(); + } //end func getFrozenHtml + + // }}} + // {{{ _getPersistantData() + + /** + * Used by getFrozenHtml() to pass the element's value if _persistantFreeze is on + * + * @access private + * @return string + */ + function _getPersistantData() + { + if (!$this->_persistantFreeze) { + return ''; + } else { + $id = $this->getAttribute('id'); + return '_getAttrString(array( + 'type' => 'hidden', + 'name' => $this->getName(), + 'value' => $this->getValue() + ) + (isset($id)? array('id' => $id): array())) . ' />'; + } + } + + // }}} + // {{{ isFrozen() + + /** + * Returns whether or not the element is frozen + * + * @since 1.3 + * @access public + * @return bool + */ + function isFrozen() + { + return $this->_flagFrozen; + } // end func isFrozen + + // }}} + // {{{ setPersistantFreeze() + + /** + * Sets wether an element value should be kept in an hidden field + * when the element is frozen or not + * + * @param bool $persistant True if persistant value + * @since 2.0 + * @access public + * @return void + */ + function setPersistantFreeze($persistant=false) + { + $this->_persistantFreeze = $persistant; + } //end func setPersistantFreeze + + // }}} + // {{{ setLabel() + + /** + * Sets display text for the element + * + * @param string $label Display text for the element + * @since 1.3 + * @access public + * @return void + */ + function setLabel($label) + { + $this->_label = $label; + } //end func setLabel + + // }}} + // {{{ getLabel() + + /** + * Returns display text for the element + * + * @since 1.3 + * @access public + * @return string + */ + function getLabel() + { + return $this->_label; + } //end func getLabel + + // }}} + // {{{ _findValue() + + /** + * Tries to find the element value from the values array + * + * @since 2.7 + * @access private + * @return mixed + */ + function _findValue(&$values) + { + if (empty($values)) { + return null; + } + $elementName = $this->getName(); + if (isset($values[$elementName])) { + return $values[$elementName]; + } elseif (strpos($elementName, '[')) { + $myVar = "['" . str_replace( + array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"), + $elementName + ) . "']"; + return eval("return (isset(\$values$myVar)) ? \$values$myVar : null;"); + } else { + return null; + } + } //end func _findValue + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'createElement': + $className = get_class($this); + $this->$className($arg[0], $arg[1], $arg[2], $arg[3], $arg[4]); + break; + case 'addElement': + $this->onQuickFormEvent('createElement', $arg, $caller); + $this->onQuickFormEvent('updateValue', null, $caller); + break; + case 'updateValue': + // constant values override both default and submitted ones + // default values are overriden by submitted + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_submitValues); + if (null === $value) { + $value = $this->_findValue($caller->_defaultValues); + } + } + if (null !== $value) { + $this->setValue($value); + } + break; + case 'setGroupValue': + $this->setValue($arg); + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ accept() + + /** + * Accepts a renderer + * + * @param HTML_QuickForm_Renderer renderer object + * @param bool Whether an element is required + * @param string An error message associated with an element + * @access public + * @return void + */ + function accept(&$renderer, $required=false, $error=null) + { + $renderer->renderElement($this, $required, $error); + } // end func accept + + // }}} + // {{{ _generateId() + + /** + * Automatically generates and assigns an 'id' attribute for the element. + * + * Currently used to ensure that labels work on radio buttons and + * checkboxes. Per idea of Alexander Radivanovich. + * + * @access private + * @return void + */ + function _generateId() + { + static $idx = 1; + + if (!$this->getAttribute('id')) { + $this->updateAttributes(array('id' => 'qf_' . substr(md5(microtime() . $idx++), 0, 6))); + } + } // end func _generateId + + // }}} + // {{{ exportValue() + + /** + * Returns a 'safe' element's value + * + * @param array array of submitted values to search + * @param bool whether to return the value as associative array + * @access public + * @return mixed + */ + function exportValue(&$submitValues, $assoc = false) + { + $value = $this->_findValue($submitValues); + if (null === $value) { + $value = $this->getValue(); + } + return $this->_prepareValue($value, $assoc); + } + + // }}} + // {{{ _prepareValue() + + /** + * Used by exportValue() to prepare the value for returning + * + * @param mixed the value found in exportValue() + * @param bool whether to return the value as associative array + * @access private + * @return mixed + */ + function _prepareValue($value, $assoc) + { + if (null === $value) { + return null; + } elseif (!$assoc) { + return $value; + } else { + $name = $this->getName(); + if (!strpos($name, '[')) { + return array($name => $value); + } else { + $valueAry = array(); + $myIndex = "['" . str_replace( + array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"), + $name + ) . "']"; + eval("\$valueAry$myIndex = \$value;"); + return $valueAry; + } + } + } + + // }}} +} // end class HTML_QuickForm_element +?> \ No newline at end of file diff --git a/HTML/QuickForm/file.php b/HTML/QuickForm/file.php new file mode 100644 index 0000000..35a186e --- /dev/null +++ b/HTML/QuickForm/file.php @@ -0,0 +1,358 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: file.php,v 1.23 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +// register file-related rules +if (class_exists('HTML_QuickForm')) { + HTML_QuickForm::registerRule('uploadedfile', 'callback', '_ruleIsUploadedFile', 'HTML_QuickForm_file'); + HTML_QuickForm::registerRule('maxfilesize', 'callback', '_ruleCheckMaxFileSize', 'HTML_QuickForm_file'); + HTML_QuickForm::registerRule('mimetype', 'callback', '_ruleCheckMimeType', 'HTML_QuickForm_file'); + HTML_QuickForm::registerRule('filename', 'callback', '_ruleCheckFileName', 'HTML_QuickForm_file'); +} + +/** + * HTML class for a file upload field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_file extends HTML_QuickForm_input +{ + // {{{ properties + + /** + * Uploaded file data, from $_FILES + * @var array + */ + var $_value = null; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string Input field name attribute + * @param string Input field label + * @param mixed (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + */ + function HTML_QuickForm_file($elementName=null, $elementLabel=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, $elementLabel, $attributes); + $this->setType('file'); + } //end constructor + + // }}} + // {{{ setSize() + + /** + * Sets size of file element + * + * @param int Size of file element + * @since 1.0 + * @access public + */ + function setSize($size) + { + $this->updateAttributes(array('size' => $size)); + } //end func setSize + + // }}} + // {{{ getSize() + + /** + * Returns size of file element + * + * @since 1.0 + * @access public + * @return int + */ + function getSize() + { + return $this->getAttribute('size'); + } //end func getSize + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return bool + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + // {{{ setValue() + + /** + * Sets value for file element. + * + * Actually this does nothing. The function is defined here to override + * HTML_Quickform_input's behaviour of setting the 'value' attribute. As + * no sane user-agent uses 's value for anything + * (because of security implications) we implement file's value as a + * read-only property with a special meaning. + * + * @param mixed Value for file element + * @since 3.0 + * @access public + */ + function setValue($value) + { + return null; + } //end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns information about the uploaded file + * + * @since 3.0 + * @access public + * @return array + */ + function getValue() + { + return $this->_value; + } // end func getValue + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string Name of event + * @param mixed event arguments + * @param object calling object + * @since 1.0 + * @access public + * @return bool + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'updateValue': + if ($caller->getAttribute('method') == 'get') { + return PEAR::raiseError('Cannot add a file upload field to a GET method form'); + } + $this->_value = $this->_findValue(); + $caller->updateAttributes(array('enctype' => 'multipart/form-data')); + $caller->setMaxFileSize(); + break; + case 'addElement': + $this->onQuickFormEvent('createElement', $arg, $caller); + return $this->onQuickFormEvent('updateValue', null, $caller); + break; + case 'createElement': + $className = get_class($this); + $this->$className($arg[0], $arg[1], $arg[2]); + break; + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ moveUploadedFile() + + /** + * Moves an uploaded file into the destination + * + * @param string Destination directory path + * @param string New file name + * @access public + * @return bool Whether the file was moved successfully + */ + function moveUploadedFile($dest, $fileName = '') + { + if ($dest != '' && substr($dest, -1) != '/') { + $dest .= '/'; + } + $fileName = ($fileName != '') ? $fileName : basename($this->_value['name']); + return move_uploaded_file($this->_value['tmp_name'], $dest . $fileName); + } // end func moveUploadedFile + + // }}} + // {{{ isUploadedFile() + + /** + * Checks if the element contains an uploaded file + * + * @access public + * @return bool true if file has been uploaded, false otherwise + */ + function isUploadedFile() + { + return $this->_ruleIsUploadedFile($this->_value); + } // end func isUploadedFile + + // }}} + // {{{ _ruleIsUploadedFile() + + /** + * Checks if the given element contains an uploaded file + * + * @param array Uploaded file info (from $_FILES) + * @access private + * @return bool true if file has been uploaded, false otherwise + */ + function _ruleIsUploadedFile($elementValue) + { + if ((isset($elementValue['error']) && $elementValue['error'] == 0) || + (!empty($elementValue['tmp_name']) && $elementValue['tmp_name'] != 'none')) { + return is_uploaded_file($elementValue['tmp_name']); + } else { + return false; + } + } // end func _ruleIsUploadedFile + + // }}} + // {{{ _ruleCheckMaxFileSize() + + /** + * Checks that the file does not exceed the max file size + * + * @param array Uploaded file info (from $_FILES) + * @param int Max file size + * @access private + * @return bool true if filesize is lower than maxsize, false otherwise + */ + function _ruleCheckMaxFileSize($elementValue, $maxSize) + { + if (!empty($elementValue['error']) && + (UPLOAD_ERR_FORM_SIZE == $elementValue['error'] || UPLOAD_ERR_INI_SIZE == $elementValue['error'])) { + return false; + } + if (!HTML_QuickForm_file::_ruleIsUploadedFile($elementValue)) { + return true; + } + return ($maxSize >= @filesize($elementValue['tmp_name'])); + } // end func _ruleCheckMaxFileSize + + // }}} + // {{{ _ruleCheckMimeType() + + /** + * Checks if the given element contains an uploaded file of the right mime type + * + * @param array Uploaded file info (from $_FILES) + * @param mixed Mime Type (can be an array of allowed types) + * @access private + * @return bool true if mimetype is correct, false otherwise + */ + function _ruleCheckMimeType($elementValue, $mimeType) + { + if (!HTML_QuickForm_file::_ruleIsUploadedFile($elementValue)) { + return true; + } + if (is_array($mimeType)) { + return in_array($elementValue['type'], $mimeType); + } + return $elementValue['type'] == $mimeType; + } // end func _ruleCheckMimeType + + // }}} + // {{{ _ruleCheckFileName() + + /** + * Checks if the given element contains an uploaded file of the filename regex + * + * @param array Uploaded file info (from $_FILES) + * @param string Regular expression + * @access private + * @return bool true if name matches regex, false otherwise + */ + function _ruleCheckFileName($elementValue, $regex) + { + if (!HTML_QuickForm_file::_ruleIsUploadedFile($elementValue)) { + return true; + } + return preg_match($regex, $elementValue['name']); + } // end func _ruleCheckFileName + + // }}} + // {{{ _findValue() + + /** + * Tries to find the element value from the values array + * + * Needs to be redefined here as $_FILES is populated differently from + * other arrays when element name is of the form foo[bar] + * + * @access private + * @return mixed + */ + function _findValue() + { + if (empty($_FILES)) { + return null; + } + $elementName = $this->getName(); + if (isset($_FILES[$elementName])) { + return $_FILES[$elementName]; + } elseif (false !== ($pos = strpos($elementName, '['))) { + $base = str_replace( + array('\\', '\''), array('\\\\', '\\\''), + substr($elementName, 0, $pos) + ); + $idx = "['" . str_replace( + array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"), + substr($elementName, $pos + 1, -1) + ) . "']"; + $props = array('name', 'type', 'size', 'tmp_name', 'error'); + $code = "if (!isset(\$_FILES['{$base}']['name']{$idx})) {\n" . + " return null;\n" . + "} else {\n" . + " \$value = array();\n"; + foreach ($props as $prop) { + $code .= " \$value['{$prop}'] = \$_FILES['{$base}']['{$prop}']{$idx};\n"; + } + return eval($code . " return \$value;\n}\n"); + } else { + return null; + } + } + + // }}} +} // end class HTML_QuickForm_file +?> diff --git a/HTML/QuickForm/group.php b/HTML/QuickForm/group.php new file mode 100644 index 0000000..e48f261 --- /dev/null +++ b/HTML/QuickForm/group.php @@ -0,0 +1,588 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: group.php,v 1.39 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * HTML class for a form element group + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_group extends HTML_QuickForm_element +{ + // {{{ properties + + /** + * Name of the element + * @var string + * @since 1.0 + * @access private + */ + var $_name = ''; + + /** + * Array of grouped elements + * @var array + * @since 1.0 + * @access private + */ + var $_elements = array(); + + /** + * String to separate elements + * @var mixed + * @since 2.5 + * @access private + */ + var $_separator = null; + + /** + * Required elements in this group + * @var array + * @since 2.5 + * @access private + */ + var $_required = array(); + + /** + * Whether to change elements' names to $groupName[$elementName] or leave them as is + * @var bool + * @since 3.0 + * @access private + */ + var $_appendName = true; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Group name + * @param array $elementLabel (optional)Group label + * @param array $elements (optional)Group elements + * @param mixed $separator (optional)Use a string for one separator, + * use an array to alternate the separators. + * @param bool $appendName (optional)whether to change elements' names to + * the form $groupName[$elementName] or leave + * them as is. + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_group($elementName=null, $elementLabel=null, $elements=null, $separator=null, $appendName = true) + { + $this->HTML_QuickForm_element($elementName, $elementLabel); + $this->_type = 'group'; + if (isset($elements) && is_array($elements)) { + $this->setElements($elements); + } + if (isset($separator)) { + $this->_separator = $separator; + } + if (isset($appendName)) { + $this->_appendName = $appendName; + } + } //end constructor + + // }}} + // {{{ setName() + + /** + * Sets the group name + * + * @param string $name Group name + * @since 1.0 + * @access public + * @return void + */ + function setName($name) + { + $this->_name = $name; + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the group name + * + * @since 1.0 + * @access public + * @return string + */ + function getName() + { + return $this->_name; + } //end func getName + + // }}} + // {{{ setValue() + + /** + * Sets values for group's elements + * + * @param mixed Values for group's elements + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + $this->_createElementsIfNotExist(); + foreach (array_keys($this->_elements) as $key) { + if (!$this->_appendName) { + $v = $this->_elements[$key]->_findValue($value); + if (null !== $v) { + $this->_elements[$key]->onQuickFormEvent('setGroupValue', $v, $this); + } + + } else { + $elementName = $this->_elements[$key]->getName(); + $index = strlen($elementName) ? $elementName : $key; + if (is_array($value)) { + if (isset($value[$index])) { + $this->_elements[$key]->onQuickFormEvent('setGroupValue', $value[$index], $this); + } + } elseif (isset($value)) { + $this->_elements[$key]->onQuickFormEvent('setGroupValue', $value, $this); + } + } + } + } //end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the group + * + * @since 1.0 + * @access public + * @return mixed + */ + function getValue() + { + $value = null; + foreach (array_keys($this->_elements) as $key) { + $element =& $this->_elements[$key]; + switch ($element->getType()) { + case 'radio': + $v = $element->getChecked()? $element->getValue(): null; + break; + case 'checkbox': + $v = $element->getChecked()? true: null; + break; + default: + $v = $element->getValue(); + } + if (null !== $v) { + $elementName = $element->getName(); + if (is_null($elementName)) { + $value = $v; + } else { + if (!is_array($value)) { + $value = is_null($value)? array(): array($value); + } + if ('' === $elementName) { + $value[] = $v; + } else { + $value[$elementName] = $v; + } + } + } + } + return $value; + } // end func getValue + + // }}} + // {{{ setElements() + + /** + * Sets the grouped elements + * + * @param array $elements Array of elements + * @since 1.1 + * @access public + * @return void + */ + function setElements($elements) + { + $this->_elements = array_values($elements); + if ($this->_flagFrozen) { + $this->freeze(); + } + } // end func setElements + + // }}} + // {{{ getElements() + + /** + * Gets the grouped elements + * + * @since 2.4 + * @access public + * @return array + */ + function &getElements() + { + $this->_createElementsIfNotExist(); + return $this->_elements; + } // end func getElements + + // }}} + // {{{ getGroupType() + + /** + * Gets the group type based on its elements + * Will return 'mixed' if elements contained in the group + * are of different types. + * + * @access public + * @return string group elements type + */ + function getGroupType() + { + $this->_createElementsIfNotExist(); + $prevType = ''; + foreach (array_keys($this->_elements) as $key) { + $type = $this->_elements[$key]->getType(); + if ($type != $prevType && $prevType != '') { + return 'mixed'; + } + $prevType = $type; + } + return $type; + } // end func getGroupType + + // }}} + // {{{ toHtml() + + /** + * Returns Html for the group + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + include_once('HTML/QuickForm/Renderer/Default.php'); + $renderer =& new HTML_QuickForm_Renderer_Default(); + $renderer->setElementTemplate('{element}'); + $this->accept($renderer); + return $renderer->toHtml(); + } //end func toHtml + + // }}} + // {{{ getElementName() + + /** + * Returns the element name inside the group such as found in the html form + * + * @param mixed $index Element name or element index in the group + * @since 3.0 + * @access public + * @return mixed string with element name, false if not found + */ + function getElementName($index) + { + $this->_createElementsIfNotExist(); + $elementName = false; + if (is_int($index) && isset($this->_elements[$index])) { + $elementName = $this->_elements[$index]->getName(); + if (isset($elementName) && $elementName == '') { + $elementName = $index; + } + if ($this->_appendName) { + if (is_null($elementName)) { + $elementName = $this->getName(); + } else { + $elementName = $this->getName().'['.$elementName.']'; + } + } + + } elseif (is_string($index)) { + foreach (array_keys($this->_elements) as $key) { + $elementName = $this->_elements[$key]->getName(); + if ($index == $elementName) { + if ($this->_appendName) { + $elementName = $this->getName().'['.$elementName.']'; + } + break; + } elseif ($this->_appendName && $this->getName().'['.$elementName.']' == $index) { + break; + } + } + } + return $elementName; + } //end func getElementName + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @since 1.3 + * @access public + * @return string + */ + function getFrozenHtml() + { + $flags = array(); + $this->_createElementsIfNotExist(); + foreach (array_keys($this->_elements) as $key) { + if (false === ($flags[$key] = $this->_elements[$key]->isFrozen())) { + $this->_elements[$key]->freeze(); + } + } + $html = $this->toHtml(); + foreach (array_keys($this->_elements) as $key) { + if (!$flags[$key]) { + $this->_elements[$key]->unfreeze(); + } + } + return $html; + } //end func getFrozenHtml + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'updateValue': + $this->_createElementsIfNotExist(); + foreach (array_keys($this->_elements) as $key) { + if ($this->_appendName) { + $elementName = $this->_elements[$key]->getName(); + if (is_null($elementName)) { + $this->_elements[$key]->setName($this->getName()); + } elseif ('' === $elementName) { + $this->_elements[$key]->setName($this->getName() . '[' . $key . ']'); + } else { + $this->_elements[$key]->setName($this->getName() . '[' . $elementName . ']'); + } + } + $this->_elements[$key]->onQuickFormEvent('updateValue', $arg, $caller); + if ($this->_appendName) { + $this->_elements[$key]->setName($elementName); + } + } + break; + + default: + parent::onQuickFormEvent($event, $arg, $caller); + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ accept() + + /** + * Accepts a renderer + * + * @param HTML_QuickForm_Renderer renderer object + * @param bool Whether a group is required + * @param string An error message associated with a group + * @access public + * @return void + */ + function accept(&$renderer, $required = false, $error = null) + { + $this->_createElementsIfNotExist(); + $renderer->startGroup($this, $required, $error); + $name = $this->getName(); + foreach (array_keys($this->_elements) as $key) { + $element =& $this->_elements[$key]; + + if ($this->_appendName) { + $elementName = $element->getName(); + if (isset($elementName)) { + $element->setName($name . '['. (strlen($elementName)? $elementName: $key) .']'); + } else { + $element->setName($name); + } + } + + $required = !$element->isFrozen() && in_array($element->getName(), $this->_required); + + $element->accept($renderer, $required); + + // restore the element's name + if ($this->_appendName) { + $element->setName($elementName); + } + } + $renderer->finishGroup($this); + } // end func accept + + // }}} + // {{{ exportValue() + + /** + * As usual, to get the group's value we access its elements and call + * their exportValue() methods + */ + function exportValue(&$submitValues, $assoc = false) + { + $value = null; + foreach (array_keys($this->_elements) as $key) { + $elementName = $this->_elements[$key]->getName(); + if ($this->_appendName) { + if (is_null($elementName)) { + $this->_elements[$key]->setName($this->getName()); + } elseif ('' === $elementName) { + $this->_elements[$key]->setName($this->getName() . '[' . $key . ']'); + } else { + $this->_elements[$key]->setName($this->getName() . '[' . $elementName . ']'); + } + } + $v = $this->_elements[$key]->exportValue($submitValues, $assoc); + if ($this->_appendName) { + $this->_elements[$key]->setName($elementName); + } + if (null !== $v) { + // Make $value an array, we will use it like one + if (null === $value) { + $value = array(); + } + if ($assoc) { + // just like HTML_QuickForm::exportValues() + $value = HTML_QuickForm::arrayMerge($value, $v); + } else { + // just like getValue(), but should work OK every time here + if (is_null($elementName)) { + $value = $v; + } elseif ('' === $elementName) { + $value[] = $v; + } else { + $value[$elementName] = $v; + } + } + } + } + // do not pass the value through _prepareValue, we took care of this already + return $value; + } + + // }}} + // {{{ _createElements() + + /** + * Creates the group's elements. + * + * This should be overriden by child classes that need to create their + * elements. The method will be called automatically when needed, calling + * it from the constructor is discouraged as the constructor is usually + * called _twice_ on element creation, first time with _no_ parameters. + * + * @access private + * @abstract + */ + function _createElements() + { + // abstract + } + + // }}} + // {{{ _createElementsIfNotExist() + + /** + * A wrapper around _createElements() + * + * This method calls _createElements() if the group's _elements array + * is empty. It also performs some updates, e.g. freezes the created + * elements if the group is already frozen. + * + * @access private + */ + function _createElementsIfNotExist() + { + if (empty($this->_elements)) { + $this->_createElements(); + if ($this->_flagFrozen) { + $this->freeze(); + } + } + } + + // }}} + // {{{ freeze() + + function freeze() + { + parent::freeze(); + foreach (array_keys($this->_elements) as $key) { + $this->_elements[$key]->freeze(); + } + } + + // }}} + // {{{ unfreeze() + + function unfreeze() + { + parent::unfreeze(); + foreach (array_keys($this->_elements) as $key) { + $this->_elements[$key]->unfreeze(); + } + } + + // }}} + // {{{ setPersistantFreeze() + + function setPersistantFreeze($persistant = false) + { + parent::setPersistantFreeze($persistant); + foreach (array_keys($this->_elements) as $key) { + $this->_elements[$key]->setPersistantFreeze($persistant); + } + } + + // }}} +} //end class HTML_QuickForm_group +?> \ No newline at end of file diff --git a/HTML/QuickForm/header.php b/HTML/QuickForm/header.php new file mode 100644 index 0000000..90e6f05 --- /dev/null +++ b/HTML/QuickForm/header.php @@ -0,0 +1,74 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: header.php,v 1.2 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * HTML class for static data + */ +require_once 'HTML/QuickForm/static.php'; + +/** + * A pseudo-element used for adding headers to form + * + * @category HTML + * @package HTML_QuickForm + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 3.0 + */ +class HTML_QuickForm_header extends HTML_QuickForm_static +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName Header name + * @param string $text Header text + * @access public + * @return void + */ + function HTML_QuickForm_header($elementName = null, $text = null) + { + $this->HTML_QuickForm_static($elementName, null, $text); + $this->_type = 'header'; + } + + // }}} + // {{{ accept() + + /** + * Accepts a renderer + * + * @param HTML_QuickForm_Renderer renderer object + * @access public + * @return void + */ + function accept(&$renderer) + { + $renderer->renderHeader($this); + } // end func accept + + // }}} + +} //end class HTML_QuickForm_header +?> diff --git a/HTML/QuickForm/hidden.php b/HTML/QuickForm/hidden.php new file mode 100644 index 0000000..95417b6 --- /dev/null +++ b/HTML/QuickForm/hidden.php @@ -0,0 +1,94 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: hidden.php,v 1.11 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a hidden type element + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_hidden extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $value (optional)Input field value + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_hidden($elementName=null, $value='', $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, null, $attributes); + $this->setType('hidden'); + $this->setValue($value); + } //end constructor + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + // {{{ accept() + + /** + * Accepts a renderer + * + * @param HTML_QuickForm_Renderer renderer object + * @access public + * @return void + */ + function accept(&$renderer) + { + $renderer->renderHidden($this); + } // end func accept + + // }}} + +} //end class HTML_QuickForm_hidden +?> diff --git a/HTML/QuickForm/hiddenselect.php b/HTML/QuickForm/hiddenselect.php new file mode 100644 index 0000000..8d3d97b --- /dev/null +++ b/HTML/QuickForm/hiddenselect.php @@ -0,0 +1,118 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: hiddenselect.php,v 1.6 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Class for elements + */ +require_once 'HTML/QuickForm/select.php'; + +/** + * Hidden select pseudo-element + * + * This class takes the same arguments as a select element, but instead + * of creating a select ring it creates hidden elements for all values + * already selected with setDefault or setConstant. This is useful if + * you have a select ring that you don't want visible, but you need all + * selected values to be passed. + * + * @category HTML + * @package HTML_QuickForm + * @author Isaac Shepard + * @version Release: 3.2.9 + * @since 2.1 + */ +class HTML_QuickForm_hiddenselect extends HTML_QuickForm_select +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string Select name attribute + * @param mixed Label(s) for the select (not used) + * @param mixed Data to be used to populate options + * @param mixed Either a typical HTML attribute string or an associative array (not used) + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_hiddenselect($elementName=null, $elementLabel=null, $options=null, $attributes=null) + { + HTML_QuickForm_element::HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->_type = 'hiddenselect'; + if (isset($options)) { + $this->load($options); + } + } //end constructor + + // }}} + // {{{ toHtml() + + /** + * Returns the SELECT in HTML + * + * @since 1.0 + * @access public + * @return string + * @throws + */ + function toHtml() + { + if (empty($this->_values)) { + return ''; + } + + $tabs = $this->_getTabs(); + $name = $this->getPrivateName(); + $strHtml = ''; + + foreach ($this->_values as $key => $val) { + for ($i = 0, $optCount = count($this->_options); $i < $optCount; $i++) { + if ($val == $this->_options[$i]['attr']['value']) { + $strHtml .= $tabs . '_getAttrString(array( + 'type' => 'hidden', + 'name' => $name, + 'value' => $val + )) . " />\n" ; + } + } + } + + return $strHtml; + } //end func toHtml + + // }}} + // {{{ accept() + + /** + * This is essentially a hidden element and should be rendered as one + */ + function accept(&$renderer) + { + $renderer->renderHidden($this); + } + + // }}} +} //end class HTML_QuickForm_hiddenselect +?> diff --git a/HTML/QuickForm/hierselect.php b/HTML/QuickForm/hierselect.php new file mode 100644 index 0000000..9e46f07 --- /dev/null +++ b/HTML/QuickForm/hierselect.php @@ -0,0 +1,593 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: hierselect.php,v 1.19 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Class for a group of form elements + */ +require_once 'HTML/QuickForm/group.php'; +/** + * Class for elements + */ +require_once 'HTML/QuickForm/select.php'; + +/** + * Hierarchical select element + * + * Class to dynamically create two or more HTML Select elements + * The first select changes the content of the second select and so on. + * This element is considered as a group. Selects will be named + * groupName[0], groupName[1], groupName[2]... + * + * @category HTML + * @package HTML_QuickForm + * @author Herim Vasquez + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 3.1 + */ +class HTML_QuickForm_hierselect extends HTML_QuickForm_group +{ + // {{{ properties + + /** + * Options for all the select elements + * + * @see setOptions() + * @var array + * @access private + */ + var $_options = array(); + + /** + * Number of select elements on this group + * + * @var int + * @access private + */ + var $_nbElements = 0; + + /** + * The javascript used to set and change the options + * + * @var string + * @access private + */ + var $_js = ''; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $elementLabel (optional)Input field label in form + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array. Date format is passed along the attributes. + * @param mixed $separator (optional)Use a string for one separator, + * use an array to alternate the separators. + * @access public + * @return void + */ + function HTML_QuickForm_hierselect($elementName=null, $elementLabel=null, $attributes=null, $separator=null) + { + $this->HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + if (isset($separator)) { + $this->_separator = $separator; + } + $this->_type = 'hierselect'; + $this->_appendName = true; + } //end constructor + + // }}} + // {{{ setOptions() + + /** + * Initialize the array structure containing the options for each select element. + * Call the functions that actually do the magic. + * + * Format is a bit more complex than for a simple select as we need to know + * which options are related to the ones in the previous select: + * + * Ex: + * + * // first select + * $select1[0] = 'Pop'; + * $select1[1] = 'Classical'; + * $select1[2] = 'Funeral doom'; + * + * // second select + * $select2[0][0] = 'Red Hot Chil Peppers'; + * $select2[0][1] = 'The Pixies'; + * $select2[1][0] = 'Wagner'; + * $select2[1][1] = 'Strauss'; + * $select2[2][0] = 'Pantheist'; + * $select2[2][1] = 'Skepticism'; + * + * // If only need two selects + * // - and using the deprecated functions + * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:'); + * $sel->setMainOptions($select1); + * $sel->setSecOptions($select2); + * + * // - and using the new setOptions function + * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:'); + * $sel->setOptions(array($select1, $select2)); + * + * // If you have a third select with prices for the cds + * $select3[0][0][0] = '15.00$'; + * $select3[0][0][1] = '17.00$'; + * // etc + * + * // You can now use + * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:'); + * $sel->setOptions(array($select1, $select2, $select3)); + * + * + * @param array $options Array of options defining each element + * @access public + * @return void + */ + function setOptions($options) + { + $this->_options = $options; + + if (empty($this->_elements)) { + $this->_nbElements = count($this->_options); + $this->_createElements(); + } else { + // setDefaults has probably been called before this function + // check if all elements have been created + $totalNbElements = count($this->_options); + for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) { + $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes()); + $this->_nbElements++; + } + } + + $this->_setOptions(); + } // end func setMainOptions + + // }}} + // {{{ setMainOptions() + + /** + * Sets the options for the first select element. Deprecated. setOptions() should be used. + * + * @param array $array Options for the first select element + * + * @access public + * @deprecated Deprecated since release 3.2.2 + * @return void + */ + function setMainOptions($array) + { + $this->_options[0] = $array; + + if (empty($this->_elements)) { + $this->_nbElements = 2; + $this->_createElements(); + } + } // end func setMainOptions + + // }}} + // {{{ setSecOptions() + + /** + * Sets the options for the second select element. Deprecated. setOptions() should be used. + * The main _options array is initialized and the _setOptions function is called. + * + * @param array $array Options for the second select element + * + * @access public + * @deprecated Deprecated since release 3.2.2 + * @return void + */ + function setSecOptions($array) + { + $this->_options[1] = $array; + + if (empty($this->_elements)) { + $this->_nbElements = 2; + $this->_createElements(); + } else { + // setDefaults has probably been called before this function + // check if all elements have been created + $totalNbElements = 2; + for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) { + $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes()); + $this->_nbElements++; + } + } + + $this->_setOptions(); + } // end func setSecOptions + + // }}} + // {{{ _setOptions() + + /** + * Sets the options for each select element + * + * @access private + * @return void + */ + function _setOptions() + { + $toLoad = ''; + foreach (array_keys($this->_elements) AS $key) { + $array = eval("return isset(\$this->_options[{$key}]{$toLoad})? \$this->_options[{$key}]{$toLoad}: null;"); + if (is_array($array)) { + $select =& $this->_elements[$key]; + $select->_options = array(); + $select->loadArray($array); + + $value = is_array($v = $select->getValue()) ? $v[0] : key($array); + $toLoad .= '[\'' . str_replace(array('\\', '\''), array('\\\\', '\\\''), $value) . '\']'; + } + } + } // end func _setOptions + + // }}} + // {{{ setValue() + + /** + * Sets values for group's elements + * + * @param array $value An array of 2 or more values, for the first, + * the second, the third etc. select + * + * @access public + * @return void + */ + function setValue($value) + { + // fix for bug #6766. Hope this doesn't break anything more + // after bug #7961. Forgot that _nbElements was used in + // _createElements() called in several places... + $this->_nbElements = max($this->_nbElements, count($value)); + parent::setValue($value); + $this->_setOptions(); + } // end func setValue + + // }}} + // {{{ _createElements() + + /** + * Creates all the elements for the group + * + * @access private + * @return void + */ + function _createElements() + { + for ($i = 0; $i < $this->_nbElements; $i++) { + $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes()); + } + } // end func _createElements + + // }}} + // {{{ toHtml() + + function toHtml() + { + $this->_js = ''; + if (!$this->_flagFrozen) { + // set the onchange attribute for each element except last + $keys = array_keys($this->_elements); + $onChange = array(); + for ($i = 0; $i < count($keys) - 1; $i++) { + $select =& $this->_elements[$keys[$i]]; + $onChange[$i] = $select->getAttribute('onchange'); + $select->updateAttributes( + array('onchange' => '_hs_swapOptions(this.form, \'' . $this->_escapeString($this->getName()) . '\', ' . $keys[$i] . ');' . $onChange[$i]) + ); + } + + // create the js function to call + if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) { + $this->_js .= <<_nbElements; $i++) { + $jsParts[] = $this->_convertArrayToJavascript($this->_options[$i]); + } + $this->_js .= "\n_hs_options['" . $this->_escapeString($this->getName()) . "'] = [\n" . + implode(",\n", $jsParts) . + "\n];\n"; + // default value; if we don't actually have any values yet just use + // the first option (for single selects) or empty array (for multiple) + $values = array(); + foreach (array_keys($this->_elements) as $key) { + if (is_array($v = $this->_elements[$key]->getValue())) { + $values[] = count($v) > 1? $v: $v[0]; + } else { + // XXX: accessing the supposedly private _options array + $values[] = $this->_elements[$key]->getMultiple() || empty($this->_elements[$key]->_options[0])? + array(): + $this->_elements[$key]->_options[0]['attr']['value']; + } + } + $this->_js .= "_hs_defaults['" . $this->_escapeString($this->getName()) . "'] = " . + $this->_convertArrayToJavascript($values, false) . ";\n"; + } + include_once('HTML/QuickForm/Renderer/Default.php'); + $renderer =& new HTML_QuickForm_Renderer_Default(); + $renderer->setElementTemplate('{element}'); + parent::accept($renderer); + + if (!empty($onChange)) { + $keys = array_keys($this->_elements); + for ($i = 0; $i < count($keys) - 1; $i++) { + $this->_elements[$keys[$i]]->updateAttributes(array('onchange' => $onChange[$i])); + } + } + return (empty($this->_js)? '': "") . + $renderer->toHtml(); + } // end func toHtml + + // }}} + // {{{ accept() + + function accept(&$renderer, $required = false, $error = null) + { + $renderer->renderElement($this, $required, $error); + } // end func accept + + // }}} + // {{{ onQuickFormEvent() + + function onQuickFormEvent($event, $arg, &$caller) + { + if ('updateValue' == $event) { + // we need to call setValue() so that the secondary option + // matches the main option + return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller); + } else { + $ret = parent::onQuickFormEvent($event, $arg, $caller); + // add onreset handler to form to properly reset hierselect (see bug #2970) + if ('addElement' == $event) { + $onReset = $caller->getAttribute('onreset'); + if (strlen($onReset)) { + if (strpos($onReset, '_hs_setupOnReset')) { + $caller->updateAttributes(array('onreset' => str_replace('_hs_setupOnReset(this, [', "_hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "', ", $onReset))); + } else { + $caller->updateAttributes(array('onreset' => "var temp = function() { {$onReset} } ; if (!temp()) { return false; } ; if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } ")); + } + } else { + $caller->updateAttributes(array('onreset' => "if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } ")); + } + } + return $ret; + } + } // end func onQuickFormEvent + + // }}} + // {{{ _convertArrayToJavascript() + + /** + * Converts PHP array to its Javascript analog + * + * @access private + * @param array PHP array to convert + * @param bool Generate Javascript object literal (default, works like PHP's associative array) or array literal + * @return string Javascript representation of the value + */ + function _convertArrayToJavascript($array, $assoc = true) + { + if (!is_array($array)) { + return $this->_convertScalarToJavascript($array); + } else { + $items = array(); + foreach ($array as $key => $val) { + $item = $assoc? "'" . $this->_escapeString($key) . "': ": ''; + if (is_array($val)) { + $item .= $this->_convertArrayToJavascript($val, $assoc); + } else { + $item .= $this->_convertScalarToJavascript($val); + } + $items[] = $item; + } + } + $js = implode(', ', $items); + return $assoc? '{ ' . $js . ' }': '[' . $js . ']'; + } + + // }}} + // {{{ _convertScalarToJavascript() + + /** + * Converts PHP's scalar value to its Javascript analog + * + * @access private + * @param mixed PHP value to convert + * @return string Javascript representation of the value + */ + function _convertScalarToJavascript($val) + { + if (is_bool($val)) { + return $val ? 'true' : 'false'; + } elseif (is_int($val) || is_double($val)) { + return $val; + } elseif (is_string($val)) { + return "'" . $this->_escapeString($val) . "'"; + } elseif (is_null($val)) { + return 'null'; + } else { + // don't bother + return '{}'; + } + } + + // }}} + // {{{ _escapeString() + + /** + * Quotes the string so that it can be used in Javascript string constants + * + * @access private + * @param string + * @return string + */ + function _escapeString($str) + { + return strtr($str,array( + "\r" => '\r', + "\n" => '\n', + "\t" => '\t', + "'" => "\\'", + '"' => '\"', + '\\' => '\\\\' + )); + } + + // }}} +} // end class HTML_QuickForm_hierselect +?> \ No newline at end of file diff --git a/HTML/QuickForm/html.php b/HTML/QuickForm/html.php new file mode 100644 index 0000000..03651a4 --- /dev/null +++ b/HTML/QuickForm/html.php @@ -0,0 +1,77 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: html.php,v 1.2 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * HTML class for static data + */ +require_once 'HTML/QuickForm/static.php'; + +/** + * A pseudo-element used for adding raw HTML to form + * + * Intended for use with the default renderer only, template-based + * ones may (and probably will) completely ignore this + * + * @category HTML + * @package HTML_QuickForm + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 3.0 + * @deprecated Please use the templates rather than add raw HTML via this element + */ +class HTML_QuickForm_html extends HTML_QuickForm_static +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $text raw HTML to add + * @access public + * @return void + */ + function HTML_QuickForm_html($text = null) + { + $this->HTML_QuickForm_static(null, null, $text); + $this->_type = 'html'; + } + + // }}} + // {{{ accept() + + /** + * Accepts a renderer + * + * @param HTML_QuickForm_Renderer renderer object (only works with Default renderer!) + * @access public + * @return void + */ + function accept(&$renderer) + { + $renderer->renderHtml($this); + } // end func accept + + // }}} + +} //end class HTML_QuickForm_html +?> diff --git a/HTML/QuickForm/image.php b/HTML/QuickForm/image.php new file mode 100644 index 0000000..0c3c3a7 --- /dev/null +++ b/HTML/QuickForm/image.php @@ -0,0 +1,127 @@ + element + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.01 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_01.txt If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: image.php,v 1.5 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for an element + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_image extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Element name attribute + * @param string $src (optional)Image source + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_image($elementName=null, $src='', $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, null, $attributes); + $this->setType('image'); + $this->setSource($src); + } // end class constructor + + // }}} + // {{{ setSource() + + /** + * Sets source for image element + * + * @param string $src source for image element + * @since 1.0 + * @access public + * @return void + */ + function setSource($src) + { + $this->updateAttributes(array('src' => $src)); + } // end func setSource + + // }}} + // {{{ setBorder() + + /** + * Sets border size for image element + * + * @param string $border border for image element + * @since 1.0 + * @access public + * @return void + */ + function setBorder($border) + { + $this->updateAttributes(array('border' => $border)); + } // end func setBorder + + // }}} + // {{{ setAlign() + + /** + * Sets alignment for image element + * + * @param string $align alignment for image element + * @since 1.0 + * @access public + * @return void + */ + function setAlign($align) + { + $this->updateAttributes(array('align' => $align)); + } // end func setAlign + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + +} // end class HTML_QuickForm_image +?> diff --git a/HTML/QuickForm/input.php b/HTML/QuickForm/input.php new file mode 100644 index 0000000..fa82558 --- /dev/null +++ b/HTML/QuickForm/input.php @@ -0,0 +1,209 @@ + form elements + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.01 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_01.txt If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: input.php,v 1.9 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * Base class for form elements + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + * @abstract + */ +class HTML_QuickForm_input extends HTML_QuickForm_element +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string Input field name attribute + * @param mixed Label(s) for the input field + * @param mixed Either a typical HTML attribute string or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_input($elementName=null, $elementLabel=null, $attributes=null) + { + $this->HTML_QuickForm_element($elementName, $elementLabel, $attributes); + } //end constructor + + // }}} + // {{{ setType() + + /** + * Sets the element type + * + * @param string $type Element type + * @since 1.0 + * @access public + * @return void + */ + function setType($type) + { + $this->_type = $type; + $this->updateAttributes(array('type'=>$type)); + } // end func setType + + // }}} + // {{{ setName() + + /** + * Sets the input field name + * + * @param string $name Input field name attribute + * @since 1.0 + * @access public + * @return void + */ + function setName($name) + { + $this->updateAttributes(array('name'=>$name)); + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @since 1.0 + * @access public + * @return string + */ + function getName() + { + return $this->getAttribute('name'); + } //end func getName + + // }}} + // {{{ setValue() + + /** + * Sets the value of the form element + * + * @param string $value Default value of the form element + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + $this->updateAttributes(array('value'=>$value)); + } // end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the form element + * + * @since 1.0 + * @access public + * @return string + */ + function getValue() + { + return $this->getAttribute('value'); + } // end func getValue + + // }}} + // {{{ toHtml() + + /** + * Returns the input field in HTML + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + if ($this->_flagFrozen) { + return $this->getFrozenHtml(); + } else { + return $this->_getTabs() . '_getAttrString($this->_attributes) . ' />'; + } + } //end func toHtml + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + * @throws + */ + function onQuickFormEvent($event, $arg, &$caller) + { + // do not use submit values for button-type elements + $type = $this->getType(); + if (('updateValue' != $event) || + ('submit' != $type && 'reset' != $type && 'image' != $type && 'button' != $type)) { + parent::onQuickFormEvent($event, $arg, $caller); + } else { + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_defaultValues); + } + if (null !== $value) { + $this->setValue($value); + } + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ exportValue() + + /** + * We don't need values from button-type elements (except submit) and files + */ + function exportValue(&$submitValues, $assoc = false) + { + $type = $this->getType(); + if ('reset' == $type || 'image' == $type || 'button' == $type || 'file' == $type) { + return null; + } else { + return parent::exportValue($submitValues, $assoc); + } + } + + // }}} +} // end class HTML_QuickForm_element +?> diff --git a/HTML/QuickForm/link.php b/HTML/QuickForm/link.php new file mode 100644 index 0000000..fd39cce --- /dev/null +++ b/HTML/QuickForm/link.php @@ -0,0 +1,200 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: link.php,v 1.3 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * HTML class for static data + */ +require_once 'HTML/QuickForm/static.php'; + +/** + * HTML class for a link type field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 2.0 + */ +class HTML_QuickForm_link extends HTML_QuickForm_static +{ + // {{{ properties + + /** + * Link display text + * @var string + * @since 1.0 + * @access private + */ + var $_text = ""; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementLabel (optional)Link label + * @param string $href (optional)Link href + * @param string $text (optional)Link display text + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + * @throws + */ + function HTML_QuickForm_link($elementName=null, $elementLabel=null, $href=null, $text=null, $attributes=null) + { + HTML_QuickForm_element::HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = false; + $this->_type = 'link'; + $this->setHref($href); + $this->_text = $text; + } //end constructor + + // }}} + // {{{ setName() + + /** + * Sets the input field name + * + * @param string $name Input field name attribute + * @since 1.0 + * @access public + * @return void + * @throws + */ + function setName($name) + { + $this->updateAttributes(array('name'=>$name)); + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @since 1.0 + * @access public + * @return string + * @throws + */ + function getName() + { + return $this->getAttribute('name'); + } //end func getName + + // }}} + // {{{ setValue() + + /** + * Sets value for textarea element + * + * @param string $value Value for password element + * @since 1.0 + * @access public + * @return void + * @throws + */ + function setValue($value) + { + return; + } //end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the form element + * + * @since 1.0 + * @access public + * @return void + * @throws + */ + function getValue() + { + return; + } // end func getValue + + + // }}} + // {{{ setHref() + + /** + * Sets the links href + * + * @param string $href + * @since 1.0 + * @access public + * @return void + * @throws + */ + function setHref($href) + { + $this->updateAttributes(array('href'=>$href)); + } // end func setHref + + // }}} + // {{{ toHtml() + + /** + * Returns the textarea element in HTML + * + * @since 1.0 + * @access public + * @return string + * @throws + */ + function toHtml() + { + $tabs = $this->_getTabs(); + $html = "$tabs_getAttrString($this->_attributes).">"; + $html .= $this->_text; + $html .= ""; + return $html; + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags (in this case, value is changed to a mask) + * + * @since 1.0 + * @access public + * @return string + * @throws + */ + function getFrozenHtml() + { + return; + } //end func getFrozenHtml + + // }}} + +} //end class HTML_QuickForm_textarea +?> diff --git a/HTML/QuickForm/password.php b/HTML/QuickForm/password.php new file mode 100644 index 0000000..a848aa0 --- /dev/null +++ b/HTML/QuickForm/password.php @@ -0,0 +1,115 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: password.php,v 1.7 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a password type field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_password extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $elementLabel (optional)Input field label + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + * @throws + */ + function HTML_QuickForm_password($elementName=null, $elementLabel=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, $elementLabel, $attributes); + $this->setType('password'); + } //end constructor + + // }}} + // {{{ setSize() + + /** + * Sets size of password element + * + * @param string $size Size of password field + * @since 1.0 + * @access public + * @return void + */ + function setSize($size) + { + $this->updateAttributes(array('size'=>$size)); + } //end func setSize + + // }}} + // {{{ setMaxlength() + + /** + * Sets maxlength of password element + * + * @param string $maxlength Maximum length of password field + * @since 1.0 + * @access public + * @return void + */ + function setMaxlength($maxlength) + { + $this->updateAttributes(array('maxlength'=>$maxlength)); + } //end func setMaxlength + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags (in this case, value is changed to a mask) + * + * @since 1.0 + * @access public + * @return string + * @throws + */ + function getFrozenHtml() + { + $value = $this->getValue(); + return ('' != $value? '**********': ' ') . + $this->_getPersistantData(); + } //end func getFrozenHtml + + // }}} + +} //end class HTML_QuickForm_password +?> diff --git a/HTML/QuickForm/radio.php b/HTML/QuickForm/radio.php new file mode 100644 index 0000000..a90f199 --- /dev/null +++ b/HTML/QuickForm/radio.php @@ -0,0 +1,251 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: radio.php,v 1.19 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a radio type element + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_radio extends HTML_QuickForm_input +{ + // {{{ properties + + /** + * Radio display text + * @var string + * @since 1.1 + * @access private + */ + var $_text = ''; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string Input field name attribute + * @param mixed Label(s) for a field + * @param string Text to display near the radio + * @param string Input field value + * @param mixed Either a typical HTML attribute string or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_radio($elementName=null, $elementLabel=null, $text=null, $value=null, $attributes=null) + { + $this->HTML_QuickForm_element($elementName, $elementLabel, $attributes); + if (isset($value)) { + $this->setValue($value); + } + $this->_persistantFreeze = true; + $this->setType('radio'); + $this->_text = $text; + $this->_generateId(); + } //end constructor + + // }}} + // {{{ setChecked() + + /** + * Sets whether radio button is checked + * + * @param bool $checked Whether the field is checked or not + * @since 1.0 + * @access public + * @return void + */ + function setChecked($checked) + { + if (!$checked) { + $this->removeAttribute('checked'); + } else { + $this->updateAttributes(array('checked'=>'checked')); + } + } //end func setChecked + + // }}} + // {{{ getChecked() + + /** + * Returns whether radio button is checked + * + * @since 1.0 + * @access public + * @return string + */ + function getChecked() + { + return $this->getAttribute('checked'); + } //end func getChecked + + // }}} + // {{{ toHtml() + + /** + * Returns the radio element in HTML + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + if (0 == strlen($this->_text)) { + $label = ''; + } elseif ($this->_flagFrozen) { + $label = $this->_text; + } else { + $label = ''; + } + return HTML_QuickForm_input::toHtml() . $label; + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @since 1.0 + * @access public + * @return string + */ + function getFrozenHtml() + { + if ($this->getChecked()) { + return '(x)' . + $this->_getPersistantData(); + } else { + return '( )'; + } + } //end func getFrozenHtml + + // }}} + // {{{ setText() + + /** + * Sets the radio text + * + * @param string $text Text to display near the radio button + * @since 1.1 + * @access public + * @return void + */ + function setText($text) + { + $this->_text = $text; + } //end func setText + + // }}} + // {{{ getText() + + /** + * Returns the radio text + * + * @since 1.1 + * @access public + * @return string + */ + function getText() + { + return $this->_text; + } //end func getText + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'updateValue': + // constant values override both default and submitted ones + // default values are overriden by submitted + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_submitValues); + if (null === $value) { + $value = $this->_findValue($caller->_defaultValues); + } + } + if (!is_null($value) && $value == $this->getValue()) { + $this->setChecked(true); + } else { + $this->setChecked(false); + } + break; + case 'setGroupValue': + if ($arg == $this->getValue()) { + $this->setChecked(true); + } else { + $this->setChecked(false); + } + break; + default: + parent::onQuickFormEvent($event, $arg, $caller); + } + return true; + } // end func onQuickFormLoad + + // }}} + // {{{ exportValue() + + /** + * Returns the value attribute if the radio is checked, null if it is not + */ + function exportValue(&$submitValues, $assoc = false) + { + $value = $this->_findValue($submitValues); + if (null === $value) { + $value = $this->getChecked()? $this->getValue(): null; + } elseif ($value != $this->getValue()) { + $value = null; + } + return $this->_prepareValue($value, $assoc); + } + + // }}} +} //end class HTML_QuickForm_radio +?> diff --git a/HTML/QuickForm/reset.php b/HTML/QuickForm/reset.php new file mode 100644 index 0000000..2f43a24 --- /dev/null +++ b/HTML/QuickForm/reset.php @@ -0,0 +1,79 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: reset.php,v 1.5 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a reset type element + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_reset extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $value (optional)Input field value + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_reset($elementName=null, $value=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, null, $attributes); + $this->setValue($value); + $this->setType('reset'); + } //end constructor + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + +} //end class HTML_QuickForm_reset +?> diff --git a/HTML/QuickForm/select.php b/HTML/QuickForm/select.php new file mode 100644 index 0000000..9c5108a --- /dev/null +++ b/HTML/QuickForm/select.php @@ -0,0 +1,614 @@ + + * @author Bertrand Mansion + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: select.php,v 1.33 2007/06/03 15:01:00 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * Class to dynamically create an HTML SELECT + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @author Alexey Borzov + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_select extends HTML_QuickForm_element { + + // {{{ properties + + /** + * Contains the select options + * + * @var array + * @since 1.0 + * @access private + */ + var $_options = array(); + + /** + * Default values of the SELECT + * + * @var string + * @since 1.0 + * @access private + */ + var $_values = null; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string Select name attribute + * @param mixed Label(s) for the select + * @param mixed Data to be used to populate options + * @param mixed Either a typical HTML attribute string or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_select($elementName=null, $elementLabel=null, $options=null, $attributes=null) + { + HTML_QuickForm_element::HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->_type = 'select'; + if (isset($options)) { + $this->load($options); + } + } //end constructor + + // }}} + // {{{ apiVersion() + + /** + * Returns the current API version + * + * @since 1.0 + * @access public + * @return double + */ + function apiVersion() + { + return 2.3; + } //end func apiVersion + + // }}} + // {{{ setSelected() + + /** + * Sets the default values of the select box + * + * @param mixed $values Array or comma delimited string of selected values + * @since 1.0 + * @access public + * @return void + */ + function setSelected($values) + { + if (is_string($values) && $this->getMultiple()) { + $values = split("[ ]?,[ ]?", $values); + } + if (is_array($values)) { + $this->_values = array_values($values); + } else { + $this->_values = array($values); + } + } //end func setSelected + + // }}} + // {{{ getSelected() + + /** + * Returns an array of the selected values + * + * @since 1.0 + * @access public + * @return array of selected values + */ + function getSelected() + { + return $this->_values; + } // end func getSelected + + // }}} + // {{{ setName() + + /** + * Sets the input field name + * + * @param string $name Input field name attribute + * @since 1.0 + * @access public + * @return void + */ + function setName($name) + { + $this->updateAttributes(array('name' => $name)); + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @since 1.0 + * @access public + * @return string + */ + function getName() + { + return $this->getAttribute('name'); + } //end func getName + + // }}} + // {{{ getPrivateName() + + /** + * Returns the element name (possibly with brackets appended) + * + * @since 1.0 + * @access public + * @return string + */ + function getPrivateName() + { + if ($this->getAttribute('multiple')) { + return $this->getName() . '[]'; + } else { + return $this->getName(); + } + } //end func getPrivateName + + // }}} + // {{{ setValue() + + /** + * Sets the value of the form element + * + * @param mixed $values Array or comma delimited string of selected values + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + $this->setSelected($value); + } // end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns an array of the selected values + * + * @since 1.0 + * @access public + * @return array of selected values + */ + function getValue() + { + return $this->_values; + } // end func getValue + + // }}} + // {{{ setSize() + + /** + * Sets the select field size, only applies to 'multiple' selects + * + * @param int $size Size of select field + * @since 1.0 + * @access public + * @return void + */ + function setSize($size) + { + $this->updateAttributes(array('size' => $size)); + } //end func setSize + + // }}} + // {{{ getSize() + + /** + * Returns the select field size + * + * @since 1.0 + * @access public + * @return int + */ + function getSize() + { + return $this->getAttribute('size'); + } //end func getSize + + // }}} + // {{{ setMultiple() + + /** + * Sets the select mutiple attribute + * + * @param bool $multiple Whether the select supports multi-selections + * @since 1.2 + * @access public + * @return void + */ + function setMultiple($multiple) + { + if ($multiple) { + $this->updateAttributes(array('multiple' => 'multiple')); + } else { + $this->removeAttribute('multiple'); + } + } //end func setMultiple + + // }}} + // {{{ getMultiple() + + /** + * Returns the select mutiple attribute + * + * @since 1.2 + * @access public + * @return bool true if multiple select, false otherwise + */ + function getMultiple() + { + return (bool)$this->getAttribute('multiple'); + } //end func getMultiple + + // }}} + // {{{ addOption() + + /** + * Adds a new OPTION to the SELECT + * + * @param string $text Display text for the OPTION + * @param string $value Value for the OPTION + * @param mixed $attributes Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function addOption($text, $value, $attributes=null) + { + if (null === $attributes) { + $attributes = array('value' => (string)$value); + } else { + $attributes = $this->_parseAttributes($attributes); + if (isset($attributes['selected'])) { + // the 'selected' attribute will be set in toHtml() + $this->_removeAttr('selected', $attributes); + if (is_null($this->_values)) { + $this->_values = array($value); + } elseif (!in_array($value, $this->_values)) { + $this->_values[] = $value; + } + } + $this->_updateAttrArray($attributes, array('value' => (string)$value)); + } + $this->_options[] = array('text' => $text, 'attr' => $attributes); + } // end func addOption + + // }}} + // {{{ loadArray() + + /** + * Loads the options from an associative array + * + * @param array $arr Associative array of options + * @param mixed $values (optional) Array or comma delimited string of selected values + * @since 1.0 + * @access public + * @return PEAR_Error on error or true + * @throws PEAR_Error + */ + function loadArray($arr, $values=null) + { + if (!is_array($arr)) { + return PEAR::raiseError('Argument 1 of HTML_Select::loadArray is not a valid array'); + } + if (isset($values)) { + $this->setSelected($values); + } + foreach ($arr as $key => $val) { + // Warning: new API since release 2.3 + $this->addOption($val, $key); + } + return true; + } // end func loadArray + + // }}} + // {{{ loadDbResult() + + /** + * Loads the options from DB_result object + * + * If no column names are specified the first two columns of the result are + * used as the text and value columns respectively + * @param object $result DB_result object + * @param string $textCol (optional) Name of column to display as the OPTION text + * @param string $valueCol (optional) Name of column to use as the OPTION value + * @param mixed $values (optional) Array or comma delimited string of selected values + * @since 1.0 + * @access public + * @return PEAR_Error on error or true + * @throws PEAR_Error + */ + function loadDbResult(&$result, $textCol=null, $valueCol=null, $values=null) + { + if (!is_object($result) || !is_a($result, 'db_result')) { + return PEAR::raiseError('Argument 1 of HTML_Select::loadDbResult is not a valid DB_result'); + } + if (isset($values)) { + $this->setValue($values); + } + $fetchMode = ($textCol && $valueCol) ? DB_FETCHMODE_ASSOC : DB_FETCHMODE_ORDERED; + while (is_array($row = $result->fetchRow($fetchMode)) ) { + if ($fetchMode == DB_FETCHMODE_ASSOC) { + $this->addOption($row[$textCol], $row[$valueCol]); + } else { + $this->addOption($row[0], $row[1]); + } + } + return true; + } // end func loadDbResult + + // }}} + // {{{ loadQuery() + + /** + * Queries a database and loads the options from the results + * + * @param mixed $conn Either an existing DB connection or a valid dsn + * @param string $sql SQL query string + * @param string $textCol (optional) Name of column to display as the OPTION text + * @param string $valueCol (optional) Name of column to use as the OPTION value + * @param mixed $values (optional) Array or comma delimited string of selected values + * @since 1.1 + * @access public + * @return void + * @throws PEAR_Error + */ + function loadQuery(&$conn, $sql, $textCol=null, $valueCol=null, $values=null) + { + if (is_string($conn)) { + require_once('DB.php'); + $dbConn = &DB::connect($conn, true); + if (DB::isError($dbConn)) { + return $dbConn; + } + } elseif (is_subclass_of($conn, "db_common")) { + $dbConn = &$conn; + } else { + return PEAR::raiseError('Argument 1 of HTML_Select::loadQuery is not a valid type'); + } + $result = $dbConn->query($sql); + if (DB::isError($result)) { + return $result; + } + $this->loadDbResult($result, $textCol, $valueCol, $values); + $result->free(); + if (is_string($conn)) { + $dbConn->disconnect(); + } + return true; + } // end func loadQuery + + // }}} + // {{{ load() + + /** + * Loads options from different types of data sources + * + * This method is a simulated overloaded method. The arguments, other than the + * first are optional and only mean something depending on the type of the first argument. + * If the first argument is an array then all arguments are passed in order to loadArray. + * If the first argument is a db_result then all arguments are passed in order to loadDbResult. + * If the first argument is a string or a DB connection then all arguments are + * passed in order to loadQuery. + * @param mixed $options Options source currently supports assoc array or DB_result + * @param mixed $param1 (optional) See function detail + * @param mixed $param2 (optional) See function detail + * @param mixed $param3 (optional) See function detail + * @param mixed $param4 (optional) See function detail + * @since 1.1 + * @access public + * @return PEAR_Error on error or true + * @throws PEAR_Error + */ + function load(&$options, $param1=null, $param2=null, $param3=null, $param4=null) + { + switch (true) { + case is_array($options): + return $this->loadArray($options, $param1); + break; + case (is_a($options, 'db_result')): + return $this->loadDbResult($options, $param1, $param2, $param3); + break; + case (is_string($options) && !empty($options) || is_subclass_of($options, "db_common")): + return $this->loadQuery($options, $param1, $param2, $param3, $param4); + break; + } + } // end func load + + // }}} + // {{{ toHtml() + + /** + * Returns the SELECT in HTML + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + if ($this->_flagFrozen) { + return $this->getFrozenHtml(); + } else { + $tabs = $this->_getTabs(); + $strHtml = ''; + + if ($this->getComment() != '') { + $strHtml .= $tabs . '\n"; + } + + if (!$this->getMultiple()) { + $attrString = $this->_getAttrString($this->_attributes); + } else { + $myName = $this->getName(); + $this->setName($myName . '[]'); + $attrString = $this->_getAttrString($this->_attributes); + $this->setName($myName); + } + $strHtml .= $tabs . '\n"; + + $strValues = is_array($this->_values)? array_map('strval', $this->_values): array(); + foreach ($this->_options as $option) { + if (!empty($strValues) && in_array($option['attr']['value'], $strValues, true)) { + $option['attr']['selected'] = 'selected'; + } + $strHtml .= $tabs . "\t_getAttrString($option['attr']) . '>' . + $option['text'] . "\n"; + } + + return $strHtml . $tabs . ''; + } + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @since 1.0 + * @access public + * @return string + */ + function getFrozenHtml() + { + $value = array(); + if (is_array($this->_values)) { + foreach ($this->_values as $key => $val) { + for ($i = 0, $optCount = count($this->_options); $i < $optCount; $i++) { + if (0 == strcmp($val, $this->_options[$i]['attr']['value'])) { + $value[$key] = $this->_options[$i]['text']; + break; + } + } + } + } + $html = empty($value)? ' ': join('
', $value); + if ($this->_persistantFreeze) { + $name = $this->getPrivateName(); + // Only use id attribute if doing single hidden input + if (1 == count($value)) { + $id = $this->getAttribute('id'); + $idAttr = isset($id)? array('id' => $id): array(); + } else { + $idAttr = array(); + } + foreach ($value as $key => $item) { + $html .= '_getAttrString(array( + 'type' => 'hidden', + 'name' => $name, + 'value' => $this->_values[$key] + ) + $idAttr) . ' />'; + } + } + return $html; + } //end func getFrozenHtml + + // }}} + // {{{ exportValue() + + /** + * We check the options and return only the values that _could_ have been + * selected. We also return a scalar value if select is not "multiple" + */ + function exportValue(&$submitValues, $assoc = false) + { + $value = $this->_findValue($submitValues); + if (is_null($value)) { + $value = $this->getValue(); + } elseif(!is_array($value)) { + $value = array($value); + } + if (is_array($value) && !empty($this->_options)) { + $cleanValue = null; + foreach ($value as $v) { + for ($i = 0, $optCount = count($this->_options); $i < $optCount; $i++) { + if (0 == strcmp($v, $this->_options[$i]['attr']['value'])) { + $cleanValue[] = $v; + break; + } + } + } + } else { + $cleanValue = $value; + } + if (is_array($cleanValue) && !$this->getMultiple()) { + return $this->_prepareValue($cleanValue[0], $assoc); + } else { + return $this->_prepareValue($cleanValue, $assoc); + } + } + + // }}} + // {{{ onQuickFormEvent() + + function onQuickFormEvent($event, $arg, &$caller) + { + if ('updateValue' == $event) { + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_submitValues); + // Fix for bug #4465 & #5269 + // XXX: should we push this to element::onQuickFormEvent()? + if (null === $value && (!$caller->isSubmitted() || !$this->getMultiple())) { + $value = $this->_findValue($caller->_defaultValues); + } + } + if (null !== $value) { + $this->setValue($value); + } + return true; + } else { + return parent::onQuickFormEvent($event, $arg, $caller); + } + } + + // }}} +} //end class HTML_QuickForm_select +?> diff --git a/HTML/QuickForm/static.php b/HTML/QuickForm/static.php new file mode 100644 index 0000000..02875e6 --- /dev/null +++ b/HTML/QuickForm/static.php @@ -0,0 +1,201 @@ + + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: static.php,v 1.7 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * HTML class for static data + * + * @category HTML + * @package HTML_QuickForm + * @author Wojciech Gdela + * @version Release: 3.2.9 + * @since 2.7 + */ +class HTML_QuickForm_static extends HTML_QuickForm_element { + + // {{{ properties + + /** + * Display text + * @var string + * @access private + */ + var $_text = null; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementLabel (optional)Label + * @param string $text (optional)Display text + * @access public + * @return void + */ + function HTML_QuickForm_static($elementName=null, $elementLabel=null, $text=null) + { + HTML_QuickForm_element::HTML_QuickForm_element($elementName, $elementLabel); + $this->_persistantFreeze = false; + $this->_type = 'static'; + $this->_text = $text; + } //end constructor + + // }}} + // {{{ setName() + + /** + * Sets the element name + * + * @param string $name Element name + * @access public + * @return void + */ + function setName($name) + { + $this->updateAttributes(array('name'=>$name)); + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @access public + * @return string + */ + function getName() + { + return $this->getAttribute('name'); + } //end func getName + + // }}} + // {{{ setText() + + /** + * Sets the text + * + * @param string $text + * @access public + * @return void + */ + function setText($text) + { + $this->_text = $text; + } // end func setText + + // }}} + // {{{ setValue() + + /** + * Sets the text (uses the standard setValue call to emulate a form element. + * + * @param string $text + * @access public + * @return void + */ + function setValue($text) + { + $this->setText($text); + } // end func setValue + + // }}} + // {{{ toHtml() + + /** + * Returns the static text element in HTML + * + * @access public + * @return string + */ + function toHtml() + { + return $this->_getTabs() . $this->_text; + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags + * + * @access public + * @return string + */ + function getFrozenHtml() + { + return $this->toHtml(); + } //end func getFrozenHtml + + // }}} + // {{{ onQuickFormEvent() + + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object &$caller calling object + * @since 1.0 + * @access public + * @return void + * @throws + */ + function onQuickFormEvent($event, $arg, &$caller) + { + switch ($event) { + case 'updateValue': + // do NOT use submitted values for static elements + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_defaultValues); + } + if (null !== $value) { + $this->setValue($value); + } + break; + default: + parent::onQuickFormEvent($event, $arg, $caller); + } + return true; + } // end func onQuickFormEvent + + // }}} + // {{{ exportValue() + + /** + * We override this here because we don't want any values from static elements + */ + function exportValue(&$submitValues, $assoc = false) + { + return null; + } + + // }}} +} //end class HTML_QuickForm_static +?> diff --git a/HTML/QuickForm/submit.php b/HTML/QuickForm/submit.php new file mode 100644 index 0000000..53156f9 --- /dev/null +++ b/HTML/QuickForm/submit.php @@ -0,0 +1,89 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: submit.php,v 1.5 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a submit type element + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_submit extends HTML_QuickForm_input +{ + // {{{ constructor + + /** + * Class constructor + * + * @param string Input field name attribute + * @param string Input field value + * @param mixed Either a typical HTML attribute string or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_submit($elementName=null, $value=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, null, $attributes); + $this->setValue($value); + $this->setType('submit'); + } //end constructor + + // }}} + // {{{ freeze() + + /** + * Freeze the element so that only its value is returned + * + * @access public + * @return void + */ + function freeze() + { + return false; + } //end func freeze + + // }}} + // {{{ exportValue() + + /** + * Only return the value if it is found within $submitValues (i.e. if + * this particular submit button was clicked) + */ + function exportValue(&$submitValues, $assoc = false) + { + return $this->_prepareValue($this->_findValue($submitValues), $assoc); + } + + // }}} +} //end class HTML_QuickForm_submit +?> diff --git a/HTML/QuickForm/text.php b/HTML/QuickForm/text.php new file mode 100644 index 0000000..cb1c10a --- /dev/null +++ b/HTML/QuickForm/text.php @@ -0,0 +1,98 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: text.php,v 1.6 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/input.php'; + +/** + * HTML class for a text field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_text extends HTML_QuickForm_input +{ + + // {{{ constructor + + /** + * Class constructor + * + * @param string $elementName (optional)Input field name attribute + * @param string $elementLabel (optional)Input field label + * @param mixed $attributes (optional)Either a typical HTML attribute string + * or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_text($elementName=null, $elementLabel=null, $attributes=null) + { + HTML_QuickForm_input::HTML_QuickForm_input($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->setType('text'); + } //end constructor + + // }}} + // {{{ setSize() + + /** + * Sets size of text field + * + * @param string $size Size of text field + * @since 1.3 + * @access public + * @return void + */ + function setSize($size) + { + $this->updateAttributes(array('size'=>$size)); + } //end func setSize + + // }}} + // {{{ setMaxlength() + + /** + * Sets maxlength of text field + * + * @param string $maxlength Maximum length of text field + * @since 1.3 + * @access public + * @return void + */ + function setMaxlength($maxlength) + { + $this->updateAttributes(array('maxlength'=>$maxlength)); + } //end func setMaxlength + + // }}} + +} //end class HTML_QuickForm_text +?> diff --git a/HTML/QuickForm/textarea.php b/HTML/QuickForm/textarea.php new file mode 100644 index 0000000..4e72b59 --- /dev/null +++ b/HTML/QuickForm/textarea.php @@ -0,0 +1,229 @@ + + * @author Bertrand Mansion + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: textarea.php,v 1.12 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * HTML class for a textarea type field + * + * @category HTML + * @package HTML_QuickForm + * @author Adam Daniel + * @author Bertrand Mansion + * @version Release: 3.2.9 + * @since 1.0 + */ +class HTML_QuickForm_textarea extends HTML_QuickForm_element +{ + // {{{ properties + + /** + * Field value + * @var string + * @since 1.0 + * @access private + */ + var $_value = null; + + // }}} + // {{{ constructor + + /** + * Class constructor + * + * @param string Input field name attribute + * @param mixed Label(s) for a field + * @param mixed Either a typical HTML attribute string or an associative array + * @since 1.0 + * @access public + * @return void + */ + function HTML_QuickForm_textarea($elementName=null, $elementLabel=null, $attributes=null) + { + HTML_QuickForm_element::HTML_QuickForm_element($elementName, $elementLabel, $attributes); + $this->_persistantFreeze = true; + $this->_type = 'textarea'; + } //end constructor + + // }}} + // {{{ setName() + + /** + * Sets the input field name + * + * @param string $name Input field name attribute + * @since 1.0 + * @access public + * @return void + */ + function setName($name) + { + $this->updateAttributes(array('name'=>$name)); + } //end func setName + + // }}} + // {{{ getName() + + /** + * Returns the element name + * + * @since 1.0 + * @access public + * @return string + */ + function getName() + { + return $this->getAttribute('name'); + } //end func getName + + // }}} + // {{{ setValue() + + /** + * Sets value for textarea element + * + * @param string $value Value for textarea element + * @since 1.0 + * @access public + * @return void + */ + function setValue($value) + { + $this->_value = $value; + } //end func setValue + + // }}} + // {{{ getValue() + + /** + * Returns the value of the form element + * + * @since 1.0 + * @access public + * @return string + */ + function getValue() + { + return $this->_value; + } // end func getValue + + // }}} + // {{{ setWrap() + + /** + * Sets wrap type for textarea element + * + * @param string $wrap Wrap type + * @since 1.0 + * @access public + * @return void + */ + function setWrap($wrap) + { + $this->updateAttributes(array('wrap' => $wrap)); + } //end func setWrap + + // }}} + // {{{ setRows() + + /** + * Sets height in rows for textarea element + * + * @param string $rows Height expressed in rows + * @since 1.0 + * @access public + * @return void + */ + function setRows($rows) + { + $this->updateAttributes(array('rows' => $rows)); + } //end func setRows + + // }}} + // {{{ setCols() + + /** + * Sets width in cols for textarea element + * + * @param string $cols Width expressed in cols + * @since 1.0 + * @access public + * @return void + */ + function setCols($cols) + { + $this->updateAttributes(array('cols' => $cols)); + } //end func setCols + + // }}} + // {{{ toHtml() + + /** + * Returns the textarea element in HTML + * + * @since 1.0 + * @access public + * @return string + */ + function toHtml() + { + if ($this->_flagFrozen) { + return $this->getFrozenHtml(); + } else { + return $this->_getTabs() . + '_getAttrString($this->_attributes) . '>' . + // because we wrap the form later we don't want the text indented + preg_replace("/(\r\n|\n|\r)/", ' ', htmlspecialchars($this->_value)) . + ''; + } + } //end func toHtml + + // }}} + // {{{ getFrozenHtml() + + /** + * Returns the value of field without HTML tags (in this case, value is changed to a mask) + * + * @since 1.0 + * @access public + * @return string + */ + function getFrozenHtml() + { + $value = htmlspecialchars($this->getValue()); + if ($this->getAttribute('wrap') == 'off') { + $html = $this->_getTabs() . '
' . $value."
\n"; + } else { + $html = nl2br($value)."\n"; + } + return $html . $this->_getPersistantData(); + } //end func getFrozenHtml + + // }}} + +} //end class HTML_QuickForm_textarea +?> diff --git a/HTML/QuickForm/xbutton.php b/HTML/QuickForm/xbutton.php new file mode 100644 index 0000000..7e4d49c --- /dev/null +++ b/HTML/QuickForm/xbutton.php @@ -0,0 +1,153 @@ + element + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.01 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_01.txt If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category HTML + * @package HTML_QuickForm + * @author Alexey Borzov + * @copyright 2001-2007 The PHP Group + * @license http://www.php.net/license/3_01.txt PHP License 3.01 + * @version CVS: $Id: xbutton.php,v 1.2 2007/05/29 18:34:36 avb Exp $ + * @link http://pear.php.net/package/HTML_QuickForm + */ + +/** + * Base class for form elements + */ +require_once 'HTML/QuickForm/element.php'; + +/** + * Class for HTML 4.0 tags) + * @param mixed Either a typical HTML attribute string or an associative array + * @access public + */ + function HTML_QuickForm_xbutton($elementName = null, $elementContent = null, $attributes = null) + { + $this->HTML_QuickForm_element($elementName, null, $attributes); + $this->setContent($elementContent); + $this->setPersistantFreeze(false); + $this->_type = 'xbutton'; + } + + + function toHtml() + { + return 'getAttributes(true) . '>' . $this->_content . ''; + } + + + function getFrozenHtml() + { + return $this->toHtml(); + } + + + function freeze() + { + return false; + } + + + function setName($name) + { + $this->updateAttributes(array( + 'name' => $name + )); + } + + + function getName() + { + return $this->getAttribute('name'); + } + + + function setValue($value) + { + $this->updateAttributes(array( + 'value' => $value + )); + } + + + function getValue() + { + return $this->getAttribute('value'); + } + + + /** + * Sets the contents of the button element + * + * @param string Button content (HTML to add between tags) + */ + function setContent($content) + { + $this->_content = $content; + } + + + function onQuickFormEvent($event, $arg, &$caller) + { + if ('updateValue' != $event) { + return parent::onQuickFormEvent($event, $arg, $caller); + } else { + $value = $this->_findValue($caller->_constantValues); + if (null === $value) { + $value = $this->_findValue($caller->_defaultValues); + } + if (null !== $value) { + $this->setValue($value); + } + } + return true; + } + + + /** + * Returns a 'safe' element's value + * + * The value is only returned if the button's type is "submit" and if this + * particlular button was clicked + */ + function exportValue(&$submitValues, $assoc = false) + { + if ('submit' == $this->getAttribute('type')) { + return $this->_prepareValue($this->_findValue($submitValues), $assoc); + } else { + return null; + } + } +} +?> diff --git a/HTTP.php b/HTTP.php new file mode 100644 index 0000000..8031c77 --- /dev/null +++ b/HTTP.php @@ -0,0 +1,359 @@ + + * @author Sterling Hughes + * @author Tomas V.V.Cox + * @author Richard Heyes + * @author Philippe Jausions + * @author Michael Wallner + * @copyright 2002-2005 The Authors + * @license BSD, revised + * @version CVS: $Id: HTTP.php,v 1.48 2005/11/08 20:11:54 mike Exp $ + * @link http://pear.php.net/package/HTTP + */ + +/** + * Miscellaneous HTTP Utilities + * + * PEAR::HTTP provides static shorthand methods for generating HTTP dates, + * issueing HTTP HEAD requests, building absolute URIs, firing redirects and + * negotiating user preferred language. + * + * @package HTTP + * @category HTTP + * @access public + * @static + * @version $Revision: 1.48 $ + */ +class HTTP +{ + /** + * Date + * + * Format a RFC compliant GMT date HTTP header. This function honors the + * "y2k_compliance" php.ini directive and formats the GMT date corresponding + * to either RFC850 or RFC822. + * + * @static + * @access public + * @return mixed GMT date string, or false for an invalid $time parameter + * @param mixed $time unix timestamp or date (default = current time) + */ + function Date($time = null) + { + if (!isset($time)) { + $time = time(); + } elseif (!is_numeric($time) && (-1 === $time = strtotime($time))) { + return false; + } + + // RFC822 or RFC850 + $format = ini_get('y2k_compliance') ? 'D, d M Y' : 'l, d-M-y'; + + return gmdate($format .' H:i:s \G\M\T', $time); + } + + /** + * Negotiate Language + * + * Negotiate language with the user's browser through the Accept-Language + * HTTP header or the user's host address. Language codes are generally in + * the form "ll" for a language spoken in only one country, or "ll-CC" for a + * language spoken in a particular country. For example, U.S. English is + * "en-US", while British English is "en-UK". Portugese as spoken in + * Portugal is "pt-PT", while Brazilian Portugese is "pt-BR". + * + * Quality factors in the Accept-Language: header are supported, e.g.: + * Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8 + * + * + * require_once 'HTTP.php'; + * $langs = array( + * 'en' => 'locales/en', + * 'en-US'=> 'locales/en', + * 'en-UK'=> 'locales/en', + * 'de' => 'locales/de', + * 'de-DE'=> 'locales/de', + * 'de-AT'=> 'locales/de', + * ); + * $neg = HTTP::negotiateLanguage($langs); + * $dir = $langs[$neg]; + * + * + * @static + * @access public + * @return string The negotiated language result or the supplied default. + * @param array $supported An associative array of supported languages, + * whose values must evaluate to true. + * @param string $default The default language to use if none is found. + */ + function negotiateLanguage($supported, $default = 'en-US') + { + $supp = array(); + foreach ($supported as $lang => $isSupported) { + if ($isSupported) { + $supp[strToLower($lang)] = $lang; + } + } + + if (!count($supp)) { + return $default; + } + + $matches = array(); + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lang) { + $lang = array_map('trim', explode(';', $lang)); + if (isset($lang[1])) { + $l = strtolower($lang[0]); + $q = (float) str_replace('q=', '', $lang[1]); + } else { + $l = strtolower($lang[0]); + $q = null; + } + if (isset($supp[$l])) { + $matches[$l] = isset($q) ? $q : 1000 - count($matches); + } + } + } + + if (count($matches)) { + asort($matches, SORT_NUMERIC); + return $supp[end($l = array_keys($matches))]; + } + + if (isset($_SERVER['REMOTE_HOST'])) { + $lang = strtolower(end($h = explode('.', $_SERVER['REMOTE_HOST']))); + if (isset($supp[$lang])) { + return $supp[$lang]; + } + } + + return $default; + } + + /** + * Head + * + * Sends a "HEAD" HTTP command to a server and returns the headers + * as an associative array. Example output could be: + * + * Array + * ( + * [response_code] => 200 // The HTTP response code + * [response] => HTTP/1.1 200 OK // The full HTTP response string + * [Date] => Fri, 11 Jan 2002 01:41:44 GMT + * [Server] => Apache/1.3.20 (Unix) PHP/4.1.1 + * [X-Powered-By] => PHP/4.1.1 + * [Connection] => close + * [Content-Type] => text/html + * ) + * + * + * @see HTTP_Client::head() + * @see HTTP_Request + * + * @static + * @access public + * @return mixed Returns associative array of response headers on success + * or PEAR error on failure. + * @param string $url A valid URL, e.g.: http://pear.php.net/credits.php + * @param integer $timeout Timeout in seconds (default = 10) + */ + function head($url, $timeout = 10) + { + $p = parse_url($url); + if (!isset($p['scheme'])) { + $p = parse_url(HTTP::absoluteURI($url)); + } elseif ($p['scheme'] != 'http') { + return HTTP::raiseError('Unsupported protocol: '. $p['scheme']); + } + + $port = isset($p['port']) ? $p['port'] : 80; + + if (!$fp = @fsockopen($p['host'], $port, $eno, $estr, $timeout)) { + return HTTP::raiseError("Connection error: $estr ($eno)"); + } + + $path = !empty($p['path']) ? $p['path'] : '/'; + $path .= !empty($p['query']) ? '?' . $p['query'] : ''; + + fputs($fp, "HEAD $path HTTP/1.0\r\n"); + fputs($fp, 'Host: ' . $p['host'] . ':' . $port . "\r\n"); + fputs($fp, "Connection: close\r\n\r\n"); + + $response = rtrim(fgets($fp, 4096)); + if (preg_match("|^HTTP/[^\s]*\s(.*?)\s|", $response, $status)) { + $headers['response_code'] = $status[1]; + } + $headers['response'] = $response; + + while ($line = fgets($fp, 4096)) { + if (!trim($line)) { + break; + } + if (($pos = strpos($line, ':')) !== false) { + $header = substr($line, 0, $pos); + $value = trim(substr($line, $pos + 1)); + $headers[$header] = $value; + } + } + fclose($fp); + return $headers; + } + + /** + * Redirect + * + * This function redirects the client. This is done by issuing + * a "Location" header and exiting if wanted. If you set $rfc2616 to true + * HTTP will output a hypertext note with the location of the redirect. + * + * @static + * @access public + * @return mixed Returns true on succes (or exits) or false if headers + * have already been sent. + * @param string $url URL where the redirect should go to. + * @param bool $exit Whether to exit immediately after redirection. + * @param bool $rfc2616 Wheter to output a hypertext note where we're + * redirecting to (Redirecting to ....) + */ + function redirect($url, $exit = true, $rfc2616 = false) + { + if (headers_sent()) { + return false; + } + + $url = HTTP::absoluteURI($url); + header('Location: '. $url); + + if ( $rfc2616 && isset($_SERVER['REQUEST_METHOD']) && + $_SERVER['REQUEST_METHOD'] != 'HEAD') { + printf('Redirecting to: %s.', $url, $url); + } + if ($exit) { + exit; + } + return true; + } + + /** + * Absolute URI + * + * This function returns the absolute URI for the partial URL passed. + * The current scheme (HTTP/HTTPS), host server, port, current script + * location are used if necessary to resolve any relative URLs. + * + * Offsets potentially created by PATH_INFO are taken care of to resolve + * relative URLs to the current script. + * + * You can choose a new protocol while resolving the URI. This is + * particularly useful when redirecting a web browser using relative URIs + * and to switch from HTTP to HTTPS, or vice-versa, at the same time. + * + * @author Philippe Jausions + * @static + * @access public + * @return string The absolute URI. + * @param string $url Absolute or relative URI the redirect should go to. + * @param string $protocol Protocol to use when redirecting URIs. + * @param integer $port A new port number. + */ + function absoluteURI($url = null, $protocol = null, $port = null) + { + // filter CR/LF + $url = str_replace(array("\r", "\n"), ' ', $url); + + // Mess around with already absolute URIs + if (preg_match('!^([a-z0-9]+)://!i', $url)) { + if (empty($protocol) && empty($port)) { + return $url; + } + if (!empty($protocol)) { + $url = $protocol .':'. end($array = explode(':', $url, 2)); + } + if (!empty($port)) { + $url = preg_replace('!^(([a-z0-9]+)://[^/:]+)(:[\d]+)?!i', + '\1:'. $port, $url); + } + return $url; + } + + $host = 'localhost'; + if (!empty($_SERVER['HTTP_HOST'])) { + list($host) = explode(':', $_SERVER['HTTP_HOST']); + } elseif (!empty($_SERVER['SERVER_NAME'])) { + list($host) = explode(':', $_SERVER['SERVER_NAME']); + } + + if (empty($protocol)) { + if (isset($_SERVER['HTTPS']) && !strcasecmp($_SERVER['HTTPS'], 'on')) { + $protocol = 'https'; + } else { + $protocol = 'http'; + } + if (!isset($port) || $port != intval($port)) { + $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; + } + } + + if ($protocol == 'http' && $port == 80) { + unset($port); + } + if ($protocol == 'https' && $port == 443) { + unset($port); + } + + $server = $protocol .'://'. $host . (isset($port) ? ':'. $port : ''); + + if (!strlen($url)) { + $url = isset($_SERVER['REQUEST_URI']) ? + $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF']; + } + + if ($url{0} == '/') { + return $server . $url; + } + + // Check for PATH_INFO + if (isset($_SERVER['PATH_INFO']) && strlen($_SERVER['PATH_INFO']) && + $_SERVER['PHP_SELF'] != $_SERVER['PATH_INFO']) { + $path = dirname(substr($_SERVER['PHP_SELF'], 0, -strlen($_SERVER['PATH_INFO']))); + } else { + $path = dirname($_SERVER['PHP_SELF']); + } + + if (substr($path = strtr($path, '\\', '/'), -1) != '/') { + $path .= '/'; + } + + return $server . $path . $url; + } + + /** + * Raise Error + * + * Lazy raising of PEAR_Errors. + * + * @static + * @access protected + * @return object PEAR_Error + * @param mixed $error + * @param int $code + */ + function raiseError($error = null, $code = null) + { + require_once 'PEAR.php'; + return PEAR::raiseError($error, $code); + } +} + +?> diff --git a/HTTP/Header.php b/HTTP/Header.php new file mode 100644 index 0000000..fd6c497 --- /dev/null +++ b/HTTP/Header.php @@ -0,0 +1,531 @@ + + * @author Davey Shafik + * @author Michael Wallner + * @copyright 2003-2005 The Authors + * @license BSD, revised + * @version CVS: $Id: Header.php,v 1.32 2005/11/08 19:06:10 mike Exp $ + * @link http://pear.php.net/package/HTTP_Header + */ + +/** + * Requires HTTP + */ +require_once 'HTTP.php'; + +/**#@+ + * Information Codes + */ +define('HTTP_HEADER_STATUS_100', '100 Continue'); +define('HTTP_HEADER_STATUS_101', '101 Switching Protocols'); +define('HTTP_HEADER_STATUS_102', '102 Processing'); +define('HTTP_HEADER_STATUS_INFORMATIONAL',1); +/**#@-*/ + +/**#+ + * Success Codes + */ +define('HTTP_HEADER_STATUS_200', '200 OK'); +define('HTTP_HEADER_STATUS_201', '201 Created'); +define('HTTP_HEADER_STATUS_202', '202 Accepted'); +define('HTTP_HEADER_STATUS_203', '203 Non-Authoritative Information'); +define('HTTP_HEADER_STATUS_204', '204 No Content'); +define('HTTP_HEADER_STATUS_205', '205 Reset Content'); +define('HTTP_HEADER_STATUS_206', '206 Partial Content'); +define('HTTP_HEADER_STATUS_207', '207 Multi-Status'); +define('HTTP_HEADER_STATUS_SUCCESSFUL',2); +/**#@-*/ + +/**#@+ + * Redirection Codes + */ +define('HTTP_HEADER_STATUS_300', '300 Multiple Choices'); +define('HTTP_HEADER_STATUS_301', '301 Moved Permanently'); +define('HTTP_HEADER_STATUS_302', '302 Found'); +define('HTTP_HEADER_STATUS_303', '303 See Other'); +define('HTTP_HEADER_STATUS_304', '304 Not Modified'); +define('HTTP_HEADER_STATUS_305', '305 Use Proxy'); +define('HTTP_HEADER_STATUS_306', '306 (Unused)'); +define('HTTP_HEADER_STATUS_307', '307 Temporary Redirect'); +define('HTTP_HEADER_STATUS_REDIRECT',3); +/**#@-*/ + +/**#@+ + * Error Codes + */ +define('HTTP_HEADER_STATUS_400', '400 Bad Request'); +define('HTTP_HEADER_STATUS_401', '401 Unauthorized'); +define('HTTP_HEADER_STATUS_402', '402 Payment Granted'); +define('HTTP_HEADER_STATUS_403', '403 Forbidden'); +define('HTTP_HEADER_STATUS_404', '404 File Not Found'); +define('HTTP_HEADER_STATUS_405', '405 Method Not Allowed'); +define('HTTP_HEADER_STATUS_406', '406 Not Acceptable'); +define('HTTP_HEADER_STATUS_407', '407 Proxy Authentication Required'); +define('HTTP_HEADER_STATUS_408', '408 Request Time-out'); +define('HTTP_HEADER_STATUS_409', '409 Conflict'); +define('HTTP_HEADER_STATUS_410', '410 Gone'); +define('HTTP_HEADER_STATUS_411', '411 Length Required'); +define('HTTP_HEADER_STATUS_412', '412 Precondition Failed'); +define('HTTP_HEADER_STATUS_413', '413 Request Entity Too Large'); +define('HTTP_HEADER_STATUS_414', '414 Request-URI Too Large'); +define('HTTP_HEADER_STATUS_415', '415 Unsupported Media Type'); +define('HTTP_HEADER_STATUS_416', '416 Requested range not satisfiable'); +define('HTTP_HEADER_STATUS_417', '417 Expectation Failed'); +define('HTTP_HEADER_STATUS_422', '422 Unprocessable Entity'); +define('HTTP_HEADER_STATUS_423', '423 Locked'); +define('HTTP_HEADER_STATUS_424', '424 Failed Dependency'); +define('HTTP_HEADER_STATUS_CLIENT_ERROR',4); +/**#@-*/ + +/**#@+ + * Server Errors + */ +define('HTTP_HEADER_STATUS_500', '500 Internal Server Error'); +define('HTTP_HEADER_STATUS_501', '501 Not Implemented'); +define('HTTP_HEADER_STATUS_502', '502 Bad Gateway'); +define('HTTP_HEADER_STATUS_503', '503 Service Unavailable'); +define('HTTP_HEADER_STATUS_504', '504 Gateway Time-out'); +define('HTTP_HEADER_STATUS_505', '505 HTTP Version not supported'); +define('HTTP_HEADER_STATUS_507', '507 Insufficient Storage'); +define('HTTP_HEADER_STATUS_SERVER_ERROR',5); +/**#@-*/ + +/** + * HTTP_Header + * + * @package HTTP_Header + * @category HTTP + * @access public + * @version $Revision: 1.32 $ + */ +class HTTP_Header extends HTTP +{ + /** + * Default Headers + * + * The values that are set as default, are the same as PHP sends by default. + * + * @var array + * @access private + */ + var $_headers = array( + 'content-type' => 'text/html', + 'pragma' => 'no-cache', + 'cache-control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' + ); + + /** + * HTTP version + * + * @var string + * @access private + */ + var $_httpVersion = '1.0'; + + /** + * Constructor + * + * Sets HTTP version. + * + * @access public + * @return object HTTP_Header + */ + function HTTP_Header() + { + if (isset($_SERVER['SERVER_PROTOCOL'])) { + $this->setHttpVersion(substr($_SERVER['SERVER_PROTOCOL'], -3)); + } + } + + /** + * Set HTTP version + * + * @access public + * @return bool Returns true on success or false if version doesn't + * match 1.0 or 1.1 (note: 1 will result in 1.0) + * @param mixed $version HTTP version, either 1.0 or 1.1 + */ + function setHttpVersion($version) + { + $version = round((float) $version, 1); + if ($version < 1.0 || $version > 1.1) { + return false; + } + $this->_httpVersion = sprintf('%0.1f', $version); + return true; + } + + /** + * Get HTTP version + * + * @access public + * @return string + */ + function getHttpVersion() + { + return $this->_httpVersion; + } + + /** + * Set Header + * + * The default value for the Last-Modified header will be current + * date and atime if $value is omitted. + * + * @access public + * @return bool Returns true on success or false if $key was empty or + * $value was not of an scalar type. + * @param string $key The name of the header. + * @param string $value The value of the header. (NULL to unset header) + */ + function setHeader($key, $value = null) + { + if (empty($key) || (isset($value) && !is_scalar($value))) { + return false; + } + + $key = strToLower($key); + if ($key == 'last-modified') { + if (!isset($value)) { + $value = HTTP::Date(time()); + } elseif (is_numeric($value)) { + $value = HTTP::Date($value); + } + } + + if (isset($value)) { + $this->_headers[$key] = $value; + } else { + unset($this->_headers[$key]); + } + + return true; + } + + /** + * Get Header + * + * If $key is omitted, all stored headers will be returned. + * + * @access public + * @return mixed Returns string value of the requested header, + * array values of all headers or false if header $key + * is not set. + * @param string $key The name of the header to fetch. + */ + function getHeader($key = null) + { + if (!isset($key)) { + return $this->_headers; + } + + $key = strToLower($key); + + if (!isset($this->_headers[$key])) { + return false; + } + + return $this->_headers[$key]; + } + + /** + * Send Headers + * + * Send out the header that you set via setHeader(). + * + * @access public + * @return bool Returns true on success or false if headers are already + * sent. + * @param array $keys Headers to (not) send, see $include. + * @param array $include If true only $keys matching headers will be + * sent, if false only header not matching $keys will be + * sent. + */ + function sendHeaders($keys = array(), $include = true) + { + if (headers_sent()) { + return false; + } + + if (count($keys)) { + array_change_key_case($keys, CASE_LOWER); + foreach ($this->_headers as $key => $value) { + if ($include ? in_array($key, $keys) : !in_array($key, $keys)) { + header($key .': '. $value); + } + } + } else { + foreach ($this->_headers as $header => $value) { + header($header .': '. $value); + } + } + return true; + } + + /** + * Send Satus Code + * + * Send out the given HTTP-Status code. Use this for example when you + * want to tell the client this page is cached, then you would call + * sendStatusCode(304). + * + * @see HTTP_Header_Cache::exitIfCached() + * + * @access public + * @return bool Returns true on success or false if headers are already + * sent. + * @param int $code The status code to send, i.e. 404, 304, 200, etc. + */ + function sendStatusCode($code) + { + if (headers_sent()) { + return false; + } + + if ($code == (int) $code && defined('HTTP_HEADER_STATUS_'. $code)) { + $code = constant('HTTP_HEADER_STATUS_'. $code); + } + + if (strncasecmp(PHP_SAPI, 'cgi', 3)) { + header('HTTP/'. $this->_httpVersion .' '. $code); + } else { + header('Status: '. $code); + } + return true; + } + + /** + * Date to Timestamp + * + * Converts dates like + * Mon, 31 Mar 2003 15:26:34 GMT + * Tue, 15 Nov 1994 12:45:26 GMT + * into a timestamp, strtotime() didn't do it in older versions. + * + * @deprecated Use PHPs strtotime() instead. + * @access public + * @return mixed Returns int unix timestamp or false if the date doesn't + * seem to be a valid GMT date. + * @param string $date The GMT date. + */ + function dateToTimestamp($date) + { + static $months = array( + null => 0, 'Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, + 'May' => 5, 'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, + 'Oct' => 10, 'Nov' => 11, 'Dec' => 12 + ); + + if (-1 < $timestamp = strToTime($date)) { + return $timestamp; + } + + if (!preg_match('~[^,]*,\s(\d+)\s(\w+)\s(\d+)\s(\d+):(\d+):(\d+).*~', + $date, $m)) { + return false; + } + + // [0] => Mon, 31 Mar 2003 15:42:55 GMT + // [1] => 31 [2] => Mar [3] => 2003 [4] => 15 [5] => 42 [6] => 55 + return mktime($m[4], $m[5], $m[6], $months[$m[2]], $m[1], $m[3]); + } + + /** + * Redirect + * + * This function redirects the client. This is done by issuing a Location + * header and exiting. Additionally to HTTP::redirect() you can also add + * parameters to the url. + * + * If you dont need parameters to be added, simply use HTTP::redirect() + * otherwise use HTTP_Header::redirect(). + * + * @see HTTP::redirect() + * @author Wolfram Kriesing + * @access public + * @return void + * @param string $url The URL to redirect to, if none is given it + * redirects to the current page. + * @param array $param Array of query string parameters to add; usually + * a set of key => value pairs; if an array entry consists + * only of an value it is used as key and the respective + * value is fetched from $GLOBALS[$value] + * @param bool $session Whether the session name/id should be added + */ + function redirect($url = null, $param = array(), $session = false) + { + if (!isset($url)) { + $url = $_SERVER['PHP_SELF']; + } + + $qs = array(); + + if ($session) { + $qs[] = session_name() .'='. session_id(); + } + + if (is_array($param) && count($param)) { + if (count($param)) { + foreach ($param as $key => $val) { + if (is_string($key)) { + $qs[] = urlencode($key) .'='. urlencode($val); + } else { + $qs[] = urlencode($val) .'='. urlencode(@$GLOBALS[$val]); + } + } + } + } + + if ($qstr = implode('&', $qs)) { + $purl = parse_url($url); + $url .= (isset($purl['query']) ? '&' : '?') . $qstr; + } + + parent::redirect($url); + } + + /**#@+ + * @author Davey Shafik + * @param int $http_code HTTP Code to check + * @access public + */ + + /** + * Return HTTP Status Code Type + * + * @return int|false + */ + function getStatusType($http_code) + { + if(is_int($http_code) && defined('HTTP_HEADER_STATUS_' .$http_code) || defined($http_code)) { + $type = substr($http_code,0,1); + switch ($type) { + case HTTP_HEADER_STATUS_INFORMATIONAL: + case HTTP_HEADER_STATUS_SUCCESSFUL: + case HTTP_HEADER_STATUS_REDIRECT: + case HTTP_HEADER_STATUS_CLIENT_ERROR: + case HTTP_HEADER_STATUS_SERVER_ERROR: + return $type; + break; + default: + return false; + break; + } + } else { + return false; + } + } + + /** + * Return Status Code Message + * + * @return string|false + */ + function getStatusText($http_code) + { + if ($this->getStatusType($http_code)) { + if (is_int($http_code) && defined('HTTP_HEADER_STATUS_' .$http_code)) { + return substr(constant('HTTP_HEADER_STATUS_' .$http_code),4); + } else { + return substr($http_code,4); + } + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is Information (1xx) + * + * @return boolean + */ + function isInformational($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return $status_type{0} == HTTP_HEADER_STATUS_INFORMATIONAL; + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is Successful (2xx) + * + * @return boolean + */ + function isSuccessful($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return $status_type{0} == HTTP_HEADER_STATUS_SUCCESSFUL; + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is a Redirect (3xx) + * + * @return boolean + */ + function isRedirect($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return $status_type{0} == HTTP_HEADER_STATUS_REDIRECT; + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is a Client Error (4xx) + * + * @return boolean + */ + function isClientError($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return $status_type{0} == HTTP_HEADER_STATUS_CLIENT_ERROR; + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is Server Error (5xx) + * + * @return boolean + */ + function isServerError($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return $status_type{0} == HTTP_HEADER_STATUS_SERVER_ERROR; + } else { + return false; + } + } + + /** + * Checks if HTTP Status code is Server OR Client Error (4xx or 5xx) + * + * @return boolean + */ + function isError($http_code) + { + if ($status_type = $this->getStatusType($http_code)) { + return (($status_type == HTTP_HEADER_STATUS_CLIENT_ERROR) || ($status_type == HTTP_HEADER_STATUS_SERVER_ERROR)) ? true : false; + } else { + return false; + } + } + /**#@-*/ +} +?> diff --git a/HTTP/Header/Cache.php b/HTTP/Header/Cache.php new file mode 100644 index 0000000..af26b76 --- /dev/null +++ b/HTTP/Header/Cache.php @@ -0,0 +1,238 @@ + + * @author Michael Wallner + * @copyright 2003-2005 The Authors + * @license BSD, revised + * @version CVS: $Id: Cache.php,v 1.24 2005/11/08 19:06:14 mike Exp $ + * @link http://pear.php.net/package/HTTP_Header + */ + +/** + * Requires HTTP_Header + */ +require_once 'HTTP/Header.php'; + +/** + * HTTP_Header_Cache + * + * This package provides methods to easier handle caching of HTTP pages. That + * means that the pages can be cached at the client (user agent or browser) and + * your application only needs to send "hey client you already have the pages". + * + * Which is done by sending the HTTP-Status "304 Not Modified", so that your + * application load and the network traffic can be reduced, since you only need + * to send the complete page once. This is really an advantage e.g. for + * generated style sheets, or simply pages that do only change rarely. + * + * Usage: + * + * require_once 'HTTP/Header/Cache.php'; + * $httpCache = new HTTP_Header_Cache(4, 'weeks'); + * $httpCache->sendHeaders(); + * // your code goes here + * + * + * @package HTTP_Header + * @category HTTP + * @access public + * @version $Revision: 1.24 $ + */ +class HTTP_Header_Cache extends HTTP_Header +{ + /** + * Constructor + * + * Set the amount of time to cache. + * + * @access public + * @return object HTTP_Header_Cache + * @param int $expires + * @param string $unit + */ + function HTTP_Header_Cache($expires = 0, $unit = 'seconds') + { + parent::HTTP_Header(); + $this->setHeader('Pragma', 'cache'); + $this->setHeader('Last-Modified', $this->getCacheStart()); + $this->setHeader('Cache-Control', 'private, must-revalidate, max-age=0'); + + if ($expires) { + if (!$this->isOlderThan($expires, $unit)) { + $this->exitCached(); + } + $this->setHeader('Last-Modified', time()); + } + } + + /** + * Get Cache Start + * + * Returns the unix timestamp of the If-Modified-Since HTTP header or the + * current time if the header was not sent by the client. + * + * @access public + * @return int unix timestamp + */ + function getCacheStart() + { + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !$this->isPost()) { + return strtotime(current($array = explode(';', + $_SERVER['HTTP_IF_MODIFIED_SINCE']))); + } + return time(); + } + + /** + * Is Older Than + * + * You can call it like this: + * + * $httpCache->isOlderThan(1, 'day'); + * $httpCache->isOlderThan(47, 'days'); + * + * $httpCache->isOlderThan(1, 'week'); + * $httpCache->isOlderThan(3, 'weeks'); + * + * $httpCache->isOlderThan(1, 'hour'); + * $httpCache->isOlderThan(5, 'hours'); + * + * $httpCache->isOlderThan(1, 'minute'); + * $httpCache->isOlderThan(15, 'minutes'); + * + * $httpCache->isOlderThan(1, 'second'); + * $httpCache->isOlderThan(15); + * + * + * If you specify something greater than "weeks" as time untit, it just + * works approximatly, because a month is taken to consist of 4.3 weeks. + * + * @access public + * @return bool Returns true if requested page is older than specified. + * @param int $time The amount of time. + * @param string $unit The unit of the time amount - (year[s], month[s], + * week[s], day[s], hour[s], minute[s], second[s]). + */ + function isOlderThan($time = 0, $unit = 'seconds') + { + if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || $this->isPost()) { + return true; + } + if (!$time) { + return false; + } + + switch (strtolower($unit)) + { + case 'year': + case 'years': + $time *= 12; + case 'month': + case 'months': + $time *= 4.3; + case 'week': + case 'weeks': + $time *= 7; + case 'day': + case 'days': + $time *= 24; + case 'hour': + case 'hours': + $time *= 60; + case 'minute': + case 'minutes': + $time *= 60; + } + + return (time() - $this->getCacheStart()) > $time; + } + + /** + * Is Cached + * + * Check whether we can consider to be cached on the client side. + * + * @access public + * @return bool Whether the page/resource is considered to be cached. + * @param int $lastModified Unix timestamp of last modification. + */ + function isCached($lastModified = 0) + { + if ($this->isPost()) { + return false; + } + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !$lastModified) { + return true; + } + if (!$seconds = time() - $lastModified) { + return false; + } + return !$this->isOlderThan($seconds); + } + + /** + * Is Post + * + * Check if request method is "POST". + * + * @access public + * @return bool + */ + function isPost() + { + return isset($_SERVER['REQUEST_METHOD']) and + 'POST' == $_SERVER['REQUEST_METHOD']; + } + + /** + * Exit If Cached + * + * Exit with "HTTP 304 Not Modified" if we consider to be cached. + * + * @access public + * @return void + * @param int $lastModified Unix timestamp of last modification. + */ + function exitIfCached($lastModified = 0) + { + if ($this->isCached($lastModified)) { + $this->exitCached(); + } + } + + /** + * Exit Cached + * + * Exit with "HTTP 304 Not Modified". + * + * @access public + * @return void + */ + function exitCached() + { + $this->sendHeaders(); + $this->sendStatusCode(304); + exit; + } + + /** + * Set Last Modified + * + * @access public + * @return void + * @param int $lastModified The unix timestamp of last modification. + */ + function setLastModified($lastModified = null) + { + $this->setHeader('Last-Modified', $lastModified); + } +} +?> diff --git a/HTTP/Request.php b/HTTP/Request.php new file mode 100644 index 0000000..67062cc --- /dev/null +++ b/HTTP/Request.php @@ -0,0 +1,1461 @@ + + * @author Alexey Borzov + * @copyright 2002-2007 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Request.php,v 1.55 2007/05/18 19:20:12 avb Exp $ + * @link http://pear.php.net/package/HTTP_Request/ + */ + +/** + * PEAR and PEAR_Error classes (for error handling) + */ +require_once 'PEAR.php'; +/** + * Socket class + */ +require_once 'Net/Socket.php'; +/** + * URL handling class + */ +require_once 'Net/URL.php'; + +/**#@+ + * Constants for HTTP request methods + */ +define('HTTP_REQUEST_METHOD_GET', 'GET', true); +define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true); +define('HTTP_REQUEST_METHOD_POST', 'POST', true); +define('HTTP_REQUEST_METHOD_PUT', 'PUT', true); +define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true); +define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true); +define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true); +/**#@-*/ + +/**#@+ + * Constants for HTTP protocol versions + */ +define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true); +define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true); +/**#@-*/ + +if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + /** + * Whether string functions are overloaded by their mbstring equivalents + */ + define('HTTP_REQUEST_MBSTRING', true); +} else { + /** + * @ignore + */ + define('HTTP_REQUEST_MBSTRING', false); +} + +/** + * Class for performing HTTP requests + * + * Simple example (fetches yahoo.com and displays it): + * + * $a = &new HTTP_Request('http://www.yahoo.com/'); + * $a->sendRequest(); + * echo $a->getResponseBody(); + * + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @version Release: 1.4.1 + */ +class HTTP_Request +{ + /**#@+ + * @access private + */ + /** + * Instance of Net_URL + * @var Net_URL + */ + var $_url; + + /** + * Type of request + * @var string + */ + var $_method; + + /** + * HTTP Version + * @var string + */ + var $_http; + + /** + * Request headers + * @var array + */ + var $_requestHeaders; + + /** + * Basic Auth Username + * @var string + */ + var $_user; + + /** + * Basic Auth Password + * @var string + */ + var $_pass; + + /** + * Socket object + * @var Net_Socket + */ + var $_sock; + + /** + * Proxy server + * @var string + */ + var $_proxy_host; + + /** + * Proxy port + * @var integer + */ + var $_proxy_port; + + /** + * Proxy username + * @var string + */ + var $_proxy_user; + + /** + * Proxy password + * @var string + */ + var $_proxy_pass; + + /** + * Post data + * @var array + */ + var $_postData; + + /** + * Request body + * @var string + */ + var $_body; + + /** + * A list of methods that MUST NOT have a request body, per RFC 2616 + * @var array + */ + var $_bodyDisallowed = array('TRACE'); + + /** + * Files to post + * @var array + */ + var $_postFiles = array(); + + /** + * Connection timeout. + * @var float + */ + var $_timeout; + + /** + * HTTP_Response object + * @var HTTP_Response + */ + var $_response; + + /** + * Whether to allow redirects + * @var boolean + */ + var $_allowRedirects; + + /** + * Maximum redirects allowed + * @var integer + */ + var $_maxRedirects; + + /** + * Current number of redirects + * @var integer + */ + var $_redirects; + + /** + * Whether to append brackets [] to array variables + * @var bool + */ + var $_useBrackets = true; + + /** + * Attached listeners + * @var array + */ + var $_listeners = array(); + + /** + * Whether to save response body in response object property + * @var bool + */ + var $_saveBody = true; + + /** + * Timeout for reading from socket (array(seconds, microseconds)) + * @var array + */ + var $_readTimeout = null; + + /** + * Options to pass to Net_Socket::connect. See stream_context_create + * @var array + */ + var $_socketOptions = null; + /**#@-*/ + + /** + * Constructor + * + * Sets up the object + * @param string The url to fetch/access + * @param array Associative array of parameters which can have the following keys: + *
    + *
  • method - Method to use, GET, POST etc (string)
  • + *
  • http - HTTP Version to use, 1.0 or 1.1 (string)
  • + *
  • user - Basic Auth username (string)
  • + *
  • pass - Basic Auth password (string)
  • + *
  • proxy_host - Proxy server host (string)
  • + *
  • proxy_port - Proxy server port (integer)
  • + *
  • proxy_user - Proxy auth username (string)
  • + *
  • proxy_pass - Proxy auth password (string)
  • + *
  • timeout - Connection timeout in seconds (float)
  • + *
  • allowRedirects - Whether to follow redirects or not (bool)
  • + *
  • maxRedirects - Max number of redirects to follow (integer)
  • + *
  • useBrackets - Whether to append [] to array variable names (bool)
  • + *
  • saveBody - Whether to save response body in response object property (bool)
  • + *
  • readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))
  • + *
  • socketOptions - Options to pass to Net_Socket object (array)
  • + *
+ * @access public + */ + function HTTP_Request($url = '', $params = array()) + { + $this->_method = HTTP_REQUEST_METHOD_GET; + $this->_http = HTTP_REQUEST_HTTP_VER_1_1; + $this->_requestHeaders = array(); + $this->_postData = array(); + $this->_body = null; + + $this->_user = null; + $this->_pass = null; + + $this->_proxy_host = null; + $this->_proxy_port = null; + $this->_proxy_user = null; + $this->_proxy_pass = null; + + $this->_allowRedirects = false; + $this->_maxRedirects = 3; + $this->_redirects = 0; + + $this->_timeout = null; + $this->_response = null; + + foreach ($params as $key => $value) { + $this->{'_' . $key} = $value; + } + + if (!empty($url)) { + $this->setURL($url); + } + + // Default useragent + $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )'); + + // We don't do keep-alives by default + $this->addHeader('Connection', 'close'); + + // Basic authentication + if (!empty($this->_user)) { + $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass)); + } + + // Proxy authentication (see bug #5913) + if (!empty($this->_proxy_user)) { + $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass)); + } + + // Use gzip encoding if possible + if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) { + $this->addHeader('Accept-Encoding', 'gzip'); + } + } + + /** + * Generates a Host header for HTTP/1.1 requests + * + * @access private + * @return string + */ + function _generateHostHeader() + { + if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) { + $host = $this->_url->host . ':' . $this->_url->port; + + } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) { + $host = $this->_url->host . ':' . $this->_url->port; + + } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) { + $host = $this->_url->host . ':' . $this->_url->port; + + } else { + $host = $this->_url->host; + } + + return $host; + } + + /** + * Resets the object to its initial state (DEPRECATED). + * Takes the same parameters as the constructor. + * + * @param string $url The url to be requested + * @param array $params Associative array of parameters + * (see constructor for details) + * @access public + * @deprecated deprecated since 1.2, call the constructor if this is necessary + */ + function reset($url, $params = array()) + { + $this->HTTP_Request($url, $params); + } + + /** + * Sets the URL to be requested + * + * @param string The url to be requested + * @access public + */ + function setURL($url) + { + $this->_url = &new Net_URL($url, $this->_useBrackets); + + if (!empty($this->_url->user) || !empty($this->_url->pass)) { + $this->setBasicAuth($this->_url->user, $this->_url->pass); + } + + if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) { + $this->addHeader('Host', $this->_generateHostHeader()); + } + + // set '/' instead of empty path rather than check later (see bug #8662) + if (empty($this->_url->path)) { + $this->_url->path = '/'; + } + } + + /** + * Returns the current request URL + * + * @return string Current request URL + * @access public + */ + function getUrl() + { + return empty($this->_url)? '': $this->_url->getUrl(); + } + + /** + * Sets a proxy to be used + * + * @param string Proxy host + * @param int Proxy port + * @param string Proxy username + * @param string Proxy password + * @access public + */ + function setProxy($host, $port = 8080, $user = null, $pass = null) + { + $this->_proxy_host = $host; + $this->_proxy_port = $port; + $this->_proxy_user = $user; + $this->_proxy_pass = $pass; + + if (!empty($user)) { + $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); + } + } + + /** + * Sets basic authentication parameters + * + * @param string Username + * @param string Password + */ + function setBasicAuth($user, $pass) + { + $this->_user = $user; + $this->_pass = $pass; + + $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); + } + + /** + * Sets the method to be used, GET, POST etc. + * + * @param string Method to use. Use the defined constants for this + * @access public + */ + function setMethod($method) + { + $this->_method = $method; + } + + /** + * Sets the HTTP version to use, 1.0 or 1.1 + * + * @param string Version to use. Use the defined constants for this + * @access public + */ + function setHttpVer($http) + { + $this->_http = $http; + } + + /** + * Adds a request header + * + * @param string Header name + * @param string Header value + * @access public + */ + function addHeader($name, $value) + { + $this->_requestHeaders[strtolower($name)] = $value; + } + + /** + * Removes a request header + * + * @param string Header name to remove + * @access public + */ + function removeHeader($name) + { + if (isset($this->_requestHeaders[strtolower($name)])) { + unset($this->_requestHeaders[strtolower($name)]); + } + } + + /** + * Adds a querystring parameter + * + * @param string Querystring parameter name + * @param string Querystring parameter value + * @param bool Whether the value is already urlencoded or not, default = not + * @access public + */ + function addQueryString($name, $value, $preencoded = false) + { + $this->_url->addQueryString($name, $value, $preencoded); + } + + /** + * Sets the querystring to literally what you supply + * + * @param string The querystring data. Should be of the format foo=bar&x=y etc + * @param bool Whether data is already urlencoded or not, default = already encoded + * @access public + */ + function addRawQueryString($querystring, $preencoded = true) + { + $this->_url->addRawQueryString($querystring, $preencoded); + } + + /** + * Adds postdata items + * + * @param string Post data name + * @param string Post data value + * @param bool Whether data is already urlencoded or not, default = not + * @access public + */ + function addPostData($name, $value, $preencoded = false) + { + if ($preencoded) { + $this->_postData[$name] = $value; + } else { + $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value); + } + } + + /** + * Recursively applies the callback function to the value + * + * @param mixed Callback function + * @param mixed Value to process + * @access private + * @return mixed Processed value + */ + function _arrayMapRecursive($callback, $value) + { + if (!is_array($value)) { + return call_user_func($callback, $value); + } else { + $map = array(); + foreach ($value as $k => $v) { + $map[$k] = $this->_arrayMapRecursive($callback, $v); + } + return $map; + } + } + + /** + * Adds a file to upload + * + * This also changes content-type to 'multipart/form-data' for proper upload + * + * @access public + * @param string name of file-upload field + * @param mixed file name(s) + * @param mixed content-type(s) of file(s) being uploaded + * @return bool true on success + * @throws PEAR_Error + */ + function addFile($inputName, $fileName, $contentType = 'application/octet-stream') + { + if (!is_array($fileName) && !is_readable($fileName)) { + return PEAR::raiseError("File '{$fileName}' is not readable"); + } elseif (is_array($fileName)) { + foreach ($fileName as $name) { + if (!is_readable($name)) { + return PEAR::raiseError("File '{$name}' is not readable"); + } + } + } + $this->addHeader('Content-Type', 'multipart/form-data'); + $this->_postFiles[$inputName] = array( + 'name' => $fileName, + 'type' => $contentType + ); + return true; + } + + /** + * Adds raw postdata (DEPRECATED) + * + * @param string The data + * @param bool Whether data is preencoded or not, default = already encoded + * @access public + * @deprecated deprecated since 1.3.0, method setBody() should be used instead + */ + function addRawPostData($postdata, $preencoded = true) + { + $this->_body = $preencoded ? $postdata : urlencode($postdata); + } + + /** + * Sets the request body (for POST, PUT and similar requests) + * + * @param string Request body + * @access public + */ + function setBody($body) + { + $this->_body = $body; + } + + /** + * Clears any postdata that has been added (DEPRECATED). + * + * Useful for multiple request scenarios. + * + * @access public + * @deprecated deprecated since 1.2 + */ + function clearPostData() + { + $this->_postData = null; + } + + /** + * Appends a cookie to "Cookie:" header + * + * @param string $name cookie name + * @param string $value cookie value + * @access public + */ + function addCookie($name, $value) + { + $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : ''; + $this->addHeader('Cookie', $cookies . $name . '=' . $value); + } + + /** + * Clears any cookies that have been added (DEPRECATED). + * + * Useful for multiple request scenarios + * + * @access public + * @deprecated deprecated since 1.2 + */ + function clearCookies() + { + $this->removeHeader('Cookie'); + } + + /** + * Sends the request + * + * @access public + * @param bool Whether to store response body in Response object property, + * set this to false if downloading a LARGE file and using a Listener + * @return mixed PEAR error on error, true otherwise + */ + function sendRequest($saveBody = true) + { + if (!is_a($this->_url, 'Net_URL')) { + return PEAR::raiseError('No URL given.'); + } + + $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host; + $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port; + + // 4.3.0 supports SSL connections using OpenSSL. The function test determines + // we running on at least 4.3.0 + if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) { + if (isset($this->_proxy_host)) { + return PEAR::raiseError('HTTPS proxies are not supported.'); + } + $host = 'ssl://' . $host; + } + + // magic quotes may fuck up file uploads and chunked response processing + $magicQuotes = ini_get('magic_quotes_runtime'); + ini_set('magic_quotes_runtime', false); + + // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive + // connection token to a proxy server... + if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) && + 'Keep-Alive' == $this->_requestHeaders['connection']) + { + $this->removeHeader('connection'); + } + + $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) || + (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']); + $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets'); + $sockKey = $host . ':' . $port; + unset($this->_sock); + + // There is a connected socket in the "static" property? + if ($keepAlive && !empty($sockets[$sockKey]) && + !empty($sockets[$sockKey]->fp)) + { + $this->_sock =& $sockets[$sockKey]; + $err = null; + } else { + $this->_notify('connect'); + $this->_sock =& new Net_Socket(); + $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions); + } + PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest()); + + if (!PEAR::isError($err)) { + if (!empty($this->_readTimeout)) { + $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]); + } + + $this->_notify('sentRequest'); + + // Read the response + $this->_response = &new HTTP_Response($this->_sock, $this->_listeners); + $err = $this->_response->process( + $this->_saveBody && $saveBody, + HTTP_REQUEST_METHOD_HEAD != $this->_method + ); + + if ($keepAlive) { + $keepAlive = (isset($this->_response->_headers['content-length']) + || (isset($this->_response->_headers['transfer-encoding']) + && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked')); + if ($keepAlive) { + if (isset($this->_response->_headers['connection'])) { + $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive'; + } else { + $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol; + } + } + } + } + + ini_set('magic_quotes_runtime', $magicQuotes); + + if (PEAR::isError($err)) { + return $err; + } + + if (!$keepAlive) { + $this->disconnect(); + // Store the connected socket in "static" property + } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) { + $sockets[$sockKey] =& $this->_sock; + } + + // Check for redirection + if ( $this->_allowRedirects + AND $this->_redirects <= $this->_maxRedirects + AND $this->getResponseCode() > 300 + AND $this->getResponseCode() < 399 + AND !empty($this->_response->_headers['location'])) { + + + $redirect = $this->_response->_headers['location']; + + // Absolute URL + if (preg_match('/^https?:\/\//i', $redirect)) { + $this->_url = &new Net_URL($redirect); + $this->addHeader('Host', $this->_generateHostHeader()); + // Absolute path + } elseif ($redirect{0} == '/') { + $this->_url->path = $redirect; + + // Relative path + } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') { + if (substr($this->_url->path, -1) == '/') { + $redirect = $this->_url->path . $redirect; + } else { + $redirect = dirname($this->_url->path) . '/' . $redirect; + } + $redirect = Net_URL::resolvePath($redirect); + $this->_url->path = $redirect; + + // Filename, no path + } else { + if (substr($this->_url->path, -1) == '/') { + $redirect = $this->_url->path . $redirect; + } else { + $redirect = dirname($this->_url->path) . '/' . $redirect; + } + $this->_url->path = $redirect; + } + + $this->_redirects++; + return $this->sendRequest($saveBody); + + // Too many redirects + } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) { + return PEAR::raiseError('Too many redirects'); + } + + return true; + } + + /** + * Disconnect the socket, if connected. Only useful if using Keep-Alive. + * + * @access public + */ + function disconnect() + { + if (!empty($this->_sock) && !empty($this->_sock->fp)) { + $this->_notify('disconnect'); + $this->_sock->disconnect(); + } + } + + /** + * Returns the response code + * + * @access public + * @return mixed Response code, false if not set + */ + function getResponseCode() + { + return isset($this->_response->_code) ? $this->_response->_code : false; + } + + /** + * Returns either the named header or all if no name given + * + * @access public + * @param string The header name to return, do not set to get all headers + * @return mixed either the value of $headername (false if header is not present) + * or an array of all headers + */ + function getResponseHeader($headername = null) + { + if (!isset($headername)) { + return isset($this->_response->_headers)? $this->_response->_headers: array(); + } else { + $headername = strtolower($headername); + return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false; + } + } + + /** + * Returns the body of the response + * + * @access public + * @return mixed response body, false if not set + */ + function getResponseBody() + { + return isset($this->_response->_body) ? $this->_response->_body : false; + } + + /** + * Returns cookies set in response + * + * @access public + * @return mixed array of response cookies, false if none are present + */ + function getResponseCookies() + { + return isset($this->_response->_cookies) ? $this->_response->_cookies : false; + } + + /** + * Builds the request string + * + * @access private + * @return string The request string + */ + function _buildRequest() + { + $separator = ini_get('arg_separator.output'); + ini_set('arg_separator.output', '&'); + $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : ''; + ini_set('arg_separator.output', $separator); + + $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : ''; + $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : ''; + $path = $this->_url->path . $querystring; + $url = $host . $port . $path; + + $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n"; + + if (in_array($this->_method, $this->_bodyDisallowed) || + (empty($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method || + (empty($this->_postData) && empty($this->_postFiles))))) + { + $this->removeHeader('Content-Type'); + } else { + if (empty($this->_requestHeaders['content-type'])) { + // Add default content-type + $this->addHeader('Content-Type', 'application/x-www-form-urlencoded'); + } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) { + $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime()); + $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary); + } + } + + // Request Headers + if (!empty($this->_requestHeaders)) { + foreach ($this->_requestHeaders as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $request .= $canonicalName . ': ' . $value . "\r\n"; + } + } + + // No post data or wrong method, so simply add a final CRLF + if (in_array($this->_method, $this->_bodyDisallowed) || + (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) { + + $request .= "\r\n"; + + // Post data if it's an array + } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && + (!empty($this->_postData) || !empty($this->_postFiles))) { + + // "normal" POST request + if (!isset($boundary)) { + $postdata = implode('&', array_map( + create_function('$a', 'return $a[0] . \'=\' . $a[1];'), + $this->_flattenArray('', $this->_postData) + )); + + // multipart request, probably with file uploads + } else { + $postdata = ''; + if (!empty($this->_postData)) { + $flatData = $this->_flattenArray('', $this->_postData); + foreach ($flatData as $item) { + $postdata .= '--' . $boundary . "\r\n"; + $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"'; + $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n"; + } + } + foreach ($this->_postFiles as $name => $value) { + if (is_array($value['name'])) { + $varname = $name . ($this->_useBrackets? '[]': ''); + } else { + $varname = $name; + $value['name'] = array($value['name']); + } + foreach ($value['name'] as $key => $filename) { + $fp = fopen($filename, 'r'); + $data = fread($fp, filesize($filename)); + fclose($fp); + $basename = basename($filename); + $type = is_array($value['type'])? @$value['type'][$key]: $value['type']; + + $postdata .= '--' . $boundary . "\r\n"; + $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"'; + $postdata .= "\r\nContent-Type: " . $type; + $postdata .= "\r\n\r\n" . $data . "\r\n"; + } + } + $postdata .= '--' . $boundary . "--\r\n"; + } + $request .= 'Content-Length: ' . + (HTTP_REQUEST_MBSTRING? mb_strlen($postdata, 'iso-8859-1'): strlen($postdata)) . + "\r\n\r\n"; + $request .= $postdata; + + // Explicitly set request body + } elseif (!empty($this->_body)) { + + $request .= 'Content-Length: ' . + (HTTP_REQUEST_MBSTRING? mb_strlen($this->_body, 'iso-8859-1'): strlen($this->_body)) . + "\r\n\r\n"; + $request .= $this->_body; + } + + return $request; + } + + /** + * Helper function to change the (probably multidimensional) associative array + * into the simple one. + * + * @param string name for item + * @param mixed item's values + * @return array array with the following items: array('item name', 'item value'); + * @access private + */ + function _flattenArray($name, $values) + { + if (!is_array($values)) { + return array(array($name, $values)); + } else { + $ret = array(); + foreach ($values as $k => $v) { + if (empty($name)) { + $newName = $k; + } elseif ($this->_useBrackets) { + $newName = $name . '[' . $k . ']'; + } else { + $newName = $name; + } + $ret = array_merge($ret, $this->_flattenArray($newName, $v)); + } + return $ret; + } + } + + + /** + * Adds a Listener to the list of listeners that are notified of + * the object's events + * + * Events sent by HTTP_Request object + * - 'connect': on connection to server + * - 'sentRequest': after the request was sent + * - 'disconnect': on disconnection from server + * + * Events sent by HTTP_Response object + * - 'gotHeaders': after receiving response headers (headers are passed in $data) + * - 'tick': on receiving a part of response body (the part is passed in $data) + * - 'gzTick': on receiving a gzip-encoded part of response body (ditto) + * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped) + * + * @param HTTP_Request_Listener listener to attach + * @return boolean whether the listener was successfully attached + * @access public + */ + function attach(&$listener) + { + if (!is_a($listener, 'HTTP_Request_Listener')) { + return false; + } + $this->_listeners[$listener->getId()] =& $listener; + return true; + } + + + /** + * Removes a Listener from the list of listeners + * + * @param HTTP_Request_Listener listener to detach + * @return boolean whether the listener was successfully detached + * @access public + */ + function detach(&$listener) + { + if (!is_a($listener, 'HTTP_Request_Listener') || + !isset($this->_listeners[$listener->getId()])) { + return false; + } + unset($this->_listeners[$listener->getId()]); + return true; + } + + + /** + * Notifies all registered listeners of an event. + * + * @param string Event name + * @param mixed Additional data + * @access private + * @see HTTP_Request::attach() + */ + function _notify($event, $data = null) + { + foreach (array_keys($this->_listeners) as $id) { + $this->_listeners[$id]->update($this, $event, $data); + } + } +} + + +/** + * Response class to complement the Request class + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @version Release: 1.4.1 + */ +class HTTP_Response +{ + /** + * Socket object + * @var Net_Socket + */ + var $_sock; + + /** + * Protocol + * @var string + */ + var $_protocol; + + /** + * Return code + * @var string + */ + var $_code; + + /** + * Response headers + * @var array + */ + var $_headers; + + /** + * Cookies set in response + * @var array + */ + var $_cookies; + + /** + * Response body + * @var string + */ + var $_body = ''; + + /** + * Used by _readChunked(): remaining length of the current chunk + * @var string + */ + var $_chunkLength = 0; + + /** + * Attached listeners + * @var array + */ + var $_listeners = array(); + + /** + * Bytes left to read from message-body + * @var null|int + */ + var $_toRead; + + /** + * Constructor + * + * @param Net_Socket socket to read the response from + * @param array listeners attached to request + */ + function HTTP_Response(&$sock, &$listeners) + { + $this->_sock =& $sock; + $this->_listeners =& $listeners; + } + + + /** + * Processes a HTTP response + * + * This extracts response code, headers, cookies and decodes body if it + * was encoded in some way + * + * @access public + * @param bool Whether to store response body in object property, set + * this to false if downloading a LARGE file and using a Listener. + * This is assumed to be true if body is gzip-encoded. + * @param bool Whether the response can actually have a message-body. + * Will be set to false for HEAD requests. + * @throws PEAR_Error + * @return mixed true on success, PEAR_Error in case of malformed response + */ + function process($saveBody = true, $canHaveBody = true) + { + do { + $line = $this->_sock->readLine(); + if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) { + return PEAR::raiseError('Malformed response.'); + } else { + $this->_protocol = 'HTTP/' . $http_version; + $this->_code = intval($returncode); + } + while ('' !== ($header = $this->_sock->readLine())) { + $this->_processHeader($header); + } + } while (100 == $this->_code); + + $this->_notify('gotHeaders', $this->_headers); + + // RFC 2616, section 4.4: + // 1. Any response message which "MUST NOT" include a message-body ... + // is always terminated by the first empty line after the header fields + // 3. ... If a message is received with both a + // Transfer-Encoding header field and a Content-Length header field, + // the latter MUST be ignored. + $canHaveBody = $canHaveBody && $this->_code >= 200 && + $this->_code != 204 && $this->_code != 304; + + // If response body is present, read it and decode + $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']); + $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']); + $hasBody = false; + if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || + 0 != $this->_headers['content-length'])) + { + if ($chunked || !isset($this->_headers['content-length'])) { + $this->_toRead = null; + } else { + $this->_toRead = $this->_headers['content-length']; + } + while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) { + if ($chunked) { + $data = $this->_readChunked(); + } elseif (is_null($this->_toRead)) { + $data = $this->_sock->read(4096); + } else { + $data = $this->_sock->read(min(4096, $this->_toRead)); + $this->_toRead -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); + } + if ('' == $data) { + break; + } else { + $hasBody = true; + if ($saveBody || $gzipped) { + $this->_body .= $data; + } + $this->_notify($gzipped? 'gzTick': 'tick', $data); + } + } + } + + if ($hasBody) { + // Uncompress the body if needed + if ($gzipped) { + $body = $this->_decodeGzip($this->_body); + if (PEAR::isError($body)) { + return $body; + } + $this->_body = $body; + $this->_notify('gotBody', $this->_body); + } else { + $this->_notify('gotBody'); + } + } + return true; + } + + + /** + * Processes the response header + * + * @access private + * @param string HTTP header + */ + function _processHeader($header) + { + if (false === strpos($header, ':')) { + return; + } + list($headername, $headervalue) = explode(':', $header, 2); + $headername = strtolower($headername); + $headervalue = ltrim($headervalue); + + if ('set-cookie' != $headername) { + if (isset($this->_headers[$headername])) { + $this->_headers[$headername] .= ',' . $headervalue; + } else { + $this->_headers[$headername] = $headervalue; + } + } else { + $this->_parseCookie($headervalue); + } + } + + + /** + * Parse a Set-Cookie header to fill $_cookies array + * + * @access private + * @param string value of Set-Cookie header + */ + function _parseCookie($headervalue) + { + $cookie = array( + 'expires' => null, + 'domain' => null, + 'path' => null, + 'secure' => false + ); + + // Only a name=value pair + if (!strpos($headervalue, ';')) { + $pos = strpos($headervalue, '='); + $cookie['name'] = trim(substr($headervalue, 0, $pos)); + $cookie['value'] = trim(substr($headervalue, $pos + 1)); + + // Some optional parameters are supplied + } else { + $elements = explode(';', $headervalue); + $pos = strpos($elements[0], '='); + $cookie['name'] = trim(substr($elements[0], 0, $pos)); + $cookie['value'] = trim(substr($elements[0], $pos + 1)); + + for ($i = 1; $i < count($elements); $i++) { + if (false === strpos($elements[$i], '=')) { + $elName = trim($elements[$i]); + $elValue = null; + } else { + list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); + } + $elName = strtolower($elName); + if ('secure' == $elName) { + $cookie['secure'] = true; + } elseif ('expires' == $elName) { + $cookie['expires'] = str_replace('"', '', $elValue); + } elseif ('path' == $elName || 'domain' == $elName) { + $cookie[$elName] = urldecode($elValue); + } else { + $cookie[$elName] = $elValue; + } + } + } + $this->_cookies[] = $cookie; + } + + + /** + * Read a part of response body encoded with chunked Transfer-Encoding + * + * @access private + * @return string + */ + function _readChunked() + { + // at start of the next chunk? + if (0 == $this->_chunkLength) { + $line = $this->_sock->readLine(); + if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + $this->_chunkLength = hexdec($matches[1]); + // Chunk with zero length indicates the end + if (0 == $this->_chunkLength) { + $this->_sock->readLine(); // make this an eof() + return ''; + } + } else { + return ''; + } + } + $data = $this->_sock->read($this->_chunkLength); + $this->_chunkLength -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); + if (0 == $this->_chunkLength) { + $this->_sock->readLine(); // Trailing CRLF + } + return $data; + } + + + /** + * Notifies all registered listeners of an event. + * + * @param string Event name + * @param mixed Additional data + * @access private + * @see HTTP_Request::_notify() + */ + function _notify($event, $data = null) + { + foreach (array_keys($this->_listeners) as $id) { + $this->_listeners[$id]->update($this, $event, $data); + } + } + + + /** + * Decodes the message-body encoded by gzip + * + * The real decoding work is done by gzinflate() built-in function, this + * method only parses the header and checks data for compliance with + * RFC 1952 + * + * @access private + * @param string gzip-encoded data + * @return string decoded data + */ + function _decodeGzip($data) + { + if (HTTP_REQUEST_MBSTRING) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('iso-8859-1'); + } + $length = strlen($data); + // If it doesn't look like gzip-encoded data, don't bother + if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { + return $data; + } + $method = ord(substr($data, 2, 1)); + if (8 != $method) { + return PEAR::raiseError('_decodeGzip(): unknown compression method'); + } + $flags = ord(substr($data, 3, 1)); + if ($flags & 224) { + return PEAR::raiseError('_decodeGzip(): reserved bits are set'); + } + + // header is 10 bytes minimum. may be longer, though. + $headerLength = 10; + // extra fields, need to skip 'em + if ($flags & 4) { + if ($length - $headerLength - 2 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $extraLength = unpack('v', substr($data, 10, 2)); + if ($length - $headerLength - 2 - $extraLength[1] < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $headerLength += $extraLength[1] + 2; + } + // file name, need to skip that + if ($flags & 8) { + if ($length - $headerLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $filenameLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $headerLength += $filenameLength + 1; + } + // comment, need to skip that also + if ($flags & 16) { + if ($length - $headerLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $commentLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $headerLength += $commentLength + 1; + } + // have a CRC for header. let's check + if ($flags & 1) { + if ($length - $headerLength - 2 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short'); + } + $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); + $crcStored = unpack('v', substr($data, $headerLength, 2)); + if ($crcReal != $crcStored[1]) { + return PEAR::raiseError('_decodeGzip(): header CRC check failed'); + } + $headerLength += 2; + } + // unpacked data CRC and size at the end of encoded data + $tmp = unpack('V2', substr($data, -8)); + $dataCrc = $tmp[1]; + $dataSize = $tmp[2]; + + // finally, call the gzinflate() function + $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize); + if (false === $unpacked) { + return PEAR::raiseError('_decodeGzip(): gzinflate() call failed'); + } elseif ($dataSize != strlen($unpacked)) { + return PEAR::raiseError('_decodeGzip(): data size check failed'); + } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { + return PEAR::raiseError('_decodeGzip(): data CRC check failed'); + } + if (HTTP_REQUEST_MBSTRING) { + mb_internal_encoding($oldEncoding); + } + return $unpacked; + } +} // End class HTTP_Response +?> diff --git a/HTTP/Request/Listener.php b/HTTP/Request/Listener.php new file mode 100644 index 0000000..f0408b0 --- /dev/null +++ b/HTTP/Request/Listener.php @@ -0,0 +1,106 @@ + + * @copyright 2002-2007 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Listener.php,v 1.3 2007/05/18 10:33:31 avb Exp $ + * @link http://pear.php.net/package/HTTP_Request/ + */ + +/** + * Listener for HTTP_Request and HTTP_Response objects + * + * This class implements the Observer part of a Subject-Observer + * design pattern. + * + * @category HTTP + * @package HTTP_Request + * @author Alexey Borzov + * @version Release: 1.4.1 + */ +class HTTP_Request_Listener +{ + /** + * A listener's identifier + * @var string + */ + var $_id; + + /** + * Constructor, sets the object's identifier + * + * @access public + */ + function HTTP_Request_Listener() + { + $this->_id = md5(uniqid('http_request_', 1)); + } + + + /** + * Returns the listener's identifier + * + * @access public + * @return string + */ + function getId() + { + return $this->_id; + } + + + /** + * This method is called when Listener is notified of an event + * + * @access public + * @param object an object the listener is attached to + * @param string Event name + * @param mixed Additional data + * @abstract + */ + function update(&$subject, $event, $data = null) + { + echo "Notified of event: '$event'\n"; + if (null !== $data) { + echo "Additional data: "; + var_dump($data); + } + } +} +?> diff --git a/HTTP/Upload.php b/HTTP/Upload.php new file mode 100644 index 0000000..2737d39 --- /dev/null +++ b/HTTP/Upload.php @@ -0,0 +1,856 @@ + http://www.gnu.org/copyleft/lesser.txt +// (c) 2001-2004 by Tomas Von Veschler Cox +// +// ********************************************** +// +// $Id: Upload.php,v 1.42 2004/08/08 09:37:50 wenz Exp $ + +/* + * Pear File Uploader class. Easy and secure managment of files + * submitted via HTML Forms. + * + * Leyend: + * - you can add error msgs in your language in the HTTP_Upload_Error class + * + * TODO: + * - try to think a way of having all the Error system in other + * file and only include it when an error ocurrs + * + * -- Notes for users HTTP_Upload >= 0.9.0 -- + * + * Error detection was enhanced, so you no longer need to + * check for PEAR::isError() in $upload->getFiles() or call + * $upload->isMissing(). Instead you'll + * get the error when do a check for $file->isError(). + * + * Example: + * + * $upload = new HTTP_Upload('en'); + * $file = $upload->getFiles('i_dont_exist_in_form_definition'); + * if ($file->isError()) { + * die($file->getMessage()); + * } + * + * -- + * + */ + +require_once 'PEAR.php'; + +/** + * defines default chmod + */ +define('HTTP_UPLOAD_DEFAULT_CHMOD', 0660); + +/** + * Error Class for HTTP_Upload + * + * @author Tomas V.V.Cox + * @see http://vulcanonet.com/soft/index.php?pack=uploader + * @package HTTP_Upload + * @category HTTP + * @access public + */ +class HTTP_Upload_Error extends PEAR +{ + /** + * Selected language for error messages + * @var string + */ + var $lang = 'en'; + + /** + * Whether HTML entities shall be encoded automatically + * @var boolean + */ + var $html = false; + + /** + * Constructor + * + * Creates a new PEAR_Error + * + * @param string $lang The language selected for error code messages + * @access public + */ + function HTTP_Upload_Error($lang = null, $html = false) + { + $this->lang = ($lang !== null) ? $lang : $this->lang; + $this->html = ($html !== false) ? $html : $this->html; + $ini_size = preg_replace('/m/i', '000000', ini_get('upload_max_filesize')); + + if (function_exists('version_compare') && + version_compare(phpversion(), '4.1', 'ge')) { + $maxsize = (isset($_POST['MAX_FILE_SIZE'])) ? + $_POST['MAX_FILE_SIZE'] : null; + } else { + global $HTTP_POST_VARS; + $maxsize = (isset($HTTP_POST_VARS['MAX_FILE_SIZE'])) ? + $HTTP_POST_VARS['MAX_FILE_SIZE'] : null; + } + + if (empty($maxsize) || ($maxsize > $ini_size)) { + $maxsize = $ini_size; + } + // XXXXX Add here error messages in your language + $this->error_codes = array( + 'TOO_LARGE' => array( + 'es' => "Fichero demasiado largo. El maximo permitido es: $maxsize bytes.", + 'en' => "File size too large. The maximum permitted size is: $maxsize bytes.", + 'de' => "Datei zu groß. Die zulässige Maximalgröße ist: $maxsize Bytes.", + 'nl' => "Het bestand is te groot, de maximale grootte is: $maxsize bytes.", + 'fr' => "Le fichier est trop gros. La taille maximum autorisée est: $maxsize bytes.", + 'it' => "Il file é troppo grande. Il massimo permesso é: $maxsize bytes.", + 'pt_BR' => "Arquivo muito grande. O tamanho máximo permitido é $maxsize bytes." + ), + 'MISSING_DIR' => array( + 'es' => 'Falta directorio destino.', + 'en' => 'Missing destination directory.', + 'de' => 'Kein Zielverzeichnis definiert.', + 'nl' => 'Geen bestemmings directory.', + 'fr' => 'Le répertoire de destination n\'est pas défini.', + 'it' => 'Manca la directory di destinazione.', + 'pt_BR' => 'Ausência de diretório de destino.' + ), + 'IS_NOT_DIR' => array( + 'es' => 'El directorio destino no existe o es un fichero regular.', + 'en' => 'The destination directory doesn\'t exist or is a regular file.', + 'de' => 'Das angebene Zielverzeichnis existiert nicht oder ist eine Datei.', + 'nl' => 'De doeldirectory bestaat niet, of is een gewoon bestand.', + 'fr' => 'Le répertoire de destination n\'existe pas ou il s\'agit d\'un fichier régulier.', + 'it' => 'La directory di destinazione non esiste o é un file.', + 'pt_BR' => 'O diretório de destino não existe ou é um arquivo.' + ), + 'NO_WRITE_PERMS' => array( + 'es' => 'El directorio destino no tiene permisos de escritura.', + 'en' => 'The destination directory doesn\'t have write perms.', + 'de' => 'Fehlende Schreibrechte für das Zielverzeichnis.', + 'nl' => 'Geen toestemming om te schrijven in de doeldirectory.', + 'fr' => 'Le répertoire de destination n\'a pas les droits en écriture.', + 'it' => 'Non si hanno i permessi di scrittura sulla directory di destinazione.', + 'pt_BR' => 'O diretório de destino não possui permissão para escrita.' + ), + 'NO_USER_FILE' => array( + 'es' => 'No se ha escogido fichero para el upload.', + 'en' => 'You haven\'t selected any file for uploading.', + 'de' => 'Es wurde keine Datei für den Upload ausgewählt.', + 'nl' => 'Er is geen bestand opgegeven om te uploaden.', + 'fr' => 'Vous n\'avez pas sélectionné de fichier à envoyer.', + 'it' => 'Nessun file selezionato per l\'upload.', + 'pt_BR' => 'Nenhum arquivo selecionado para upload.' + ), + 'BAD_FORM' => array( + 'es' => 'El formulario no contiene method="post" enctype="multipart/form-data" requerido.', + 'en' => 'The html form doesn\'t contain the required method="post" enctype="multipart/form-data".', + 'de' => 'Das HTML-Formular enthält nicht die Angabe method="post" enctype="multipart/form-data" '. + 'im >form<-Tag.', + 'nl' => 'Het HTML-formulier bevat niet de volgende benodigde '. + 'eigenschappen: method="post" enctype="multipart/form-data".', + 'fr' => 'Le formulaire HTML ne contient pas les attributs requis : '. + ' method="post" enctype="multipart/form-data".', + 'it' => 'Il modulo HTML non contiene gli attributi richiesti: "'. + ' method="post" enctype="multipart/form-data".', + 'pt_BR' => 'O formulário HTML não possui o method="post" enctype="multipart/form-data" requerido.' + ), + 'E_FAIL_COPY' => array( + 'es' => 'Fallo al copiar el fichero temporal.', + 'en' => 'Failed to copy the temporary file.', + 'de' => 'Temporäre Datei konnte nicht kopiert werden.', + 'nl' => 'Het tijdelijke bestand kon niet gekopieerd worden.', + 'fr' => 'L\'enregistrement du fichier temporaire a échoué.', + 'it' => 'Copia del file temporaneo fallita.', + 'pt_BR' => 'Falha ao copiar o arquivo temporário.' + ), + 'E_FAIL_MOVE' => array( + 'es' => 'No puedo mover el fichero.', + 'en' => 'Impossible to move the file.', + 'de' => 'Datei kann nicht verschoben werden.', + 'nl' => 'Het bestand kon niet verplaatst worden.', + 'fr' => 'Impossible de déplacer le fichier.', + 'pt_BR' => 'Não foi possível mover o arquivo.' + ), + 'FILE_EXISTS' => array( + 'es' => 'El fichero destino ya existe.', + 'en' => 'The destination file already exists.', + 'de' => 'Die zu erzeugende Datei existiert bereits.', + 'nl' => 'Het doelbestand bestaat al.', + 'fr' => 'Le fichier de destination existe déjà.', + 'it' => 'File destinazione già esistente.', + 'pt_BR' => 'O arquivo de destino já existe.' + ), + 'CANNOT_OVERWRITE' => array( + 'es' => 'El fichero destino ya existe y no se puede sobreescribir.', + 'en' => 'The destination file already exists and could not be overwritten.', + 'de' => 'Die zu erzeugende Datei existiert bereits und konnte nicht überschrieben werden.', + 'nl' => 'Het doelbestand bestaat al, en kon niet worden overschreven.', + 'fr' => 'Le fichier de destination existe déjà et ne peux pas être remplacé.', + 'it' => 'File destinazione già esistente e non si può sovrascrivere.', + 'pt_BR' => 'O arquivo de destino já existe e não pôde ser sobrescrito.' + ), + 'NOT_ALLOWED_EXTENSION' => array( + 'es' => 'Extension de fichero no permitida.', + 'en' => 'File extension not permitted.', + 'de' => 'Unerlaubte Dateiendung.', + 'nl' => 'Niet toegestane bestands-extensie.', + 'fr' => 'Le fichier a une extension non autorisée.', + 'it' => 'Estensione del File non permessa.', + 'pt_BR' => 'Extensão de arquivo não permitida.' + ), + 'PARTIAL' => array( + 'es' => 'El fichero fue parcialmente subido', + 'en' => 'The file was only partially uploaded.', + 'de' => 'Die Datei wurde unvollständig übertragen.', + 'nl' => 'Het bestand is slechts gedeeltelijk geupload.', + 'pt_BR' => 'O arquivo não foi enviado por completo.' + ), + 'ERROR' => array( + 'es' => 'Error en subida:', + 'en' => 'Upload error:', + 'de' => 'Fehler beim Upload:', + 'nl' => 'Upload fout:', + 'pt_BR' => 'Erro de upload:' + ), + 'DEV_NO_DEF_FILE' => array( + 'es' => 'No está definido en el formulario este nombre de fichero como <input type="file" name=?>.', + 'en' => 'This filename is not defined in the form as <input type="file" name=?>.', + 'de' => 'Dieser Dateiname ist im Formular nicht als <input type="file" name=?> definiert.', + 'nl' => 'Deze bestandsnaam is niett gedefineerd in het formulier als <input type="file" name=?>.' + ) + ); + } + + /** + * returns the error code + * + * @param string $e_code type of error + * @return string Error message + */ + function errorCode($e_code) + { + if (!empty($this->error_codes[$e_code][$this->lang])) { + $msg = $this->html ? + html_entity_decode($this->error_codes[$e_code][$this->lang]) : + $this->error_codes[$e_code][$this->lang]; + } else { + $msg = $e_code; + } + + if (!empty($this->error_codes['ERROR'][$this->lang])) { + $error = $this->error_codes['ERROR'][$this->lang]; + } else { + $error = $this->error_codes['ERROR']['en']; + } + return $error.' '.$msg; + } + + /** + * Overwrites the PEAR::raiseError method + * + * @param string $e_code type of error + * @return object PEAR_Error a PEAR-Error object + * @access public + */ + function raiseError($e_code) + { + return PEAR::raiseError($this->errorCode($e_code), $e_code); + } +} + +/** + * This class provides an advanced file uploader system + * for file uploads made from html forms + + * + * @author Tomas V.V.Cox + * @see http://vulcanonet.com/soft/index.php?pack=uploader + * @package HTTP_Upload + * @category HTTP + * @access public + */ +class HTTP_Upload extends HTTP_Upload_Error +{ + /** + * Contains an array of "uploaded files" objects + * @var array + */ + var $files = array(); + + /** + * Contains the desired chmod for uploaded files + * @var int + * @access private + */ + var $_chmod = HTTP_UPLOAD_DEFAULT_CHMOD; + + /** + * Constructor + * + * @param string $lang Language to use for reporting errors + * @see Upload_Error::error_codes + * @access public + */ + function HTTP_Upload($lang = null) + { + $this->HTTP_Upload_Error($lang); + if (function_exists('version_compare') && + version_compare(phpversion(), '4.1', 'ge')) + { + $this->post_files = $_FILES; + if (isset($_SERVER['CONTENT_TYPE'])) { + $this->content_type = $_SERVER['CONTENT_TYPE']; + } + } else { + global $HTTP_POST_FILES, $HTTP_SERVER_VARS; + $this->post_files = $HTTP_POST_FILES; + if (isset($HTTP_SERVER_VARS['CONTENT_TYPE'])) { + $this->content_type = $HTTP_SERVER_VARS['CONTENT_TYPE']; + } + } + } + + /** + * Get files + * + * @param mixed $file If: + * - not given, function will return array of upload_file objects + * - is int, will return the $file position in upload_file objects array + * - is string, will return the upload_file object corresponding + * to $file name of the form. For ex: + * if form is + * to get this file use: $upload->getFiles('userfile') + * + * @return mixed array or object (see @param $file above) or Pear_Error + * @access public + */ + function &getFiles($file = null) + { + static $is_built = false; + //build only once for multiple calls + if (!$is_built) { + $files = &$this->_buildFiles(); + if (PEAR::isError($files)) { + // there was an error with the form. + // Create a faked upload embedding the error + $this->files['_error'] = &new HTTP_Upload_File( + '_error', null, + null, null, + null, $files->getCode(), + $this->lang, $this->_chmod); + } else { + $this->files = $files; + } + $is_built = true; + } + if ($file !== null) { + if (is_int($file)) { + $pos = 0; + foreach ($this->files as $obj) { + if ($pos == $file) { + return $obj; + } + $pos++; + } + } elseif (is_string($file) && isset($this->files[$file])) { + return $this->files[$file]; + } + if (isset($this->files['_error'])) { + return $this->files['_error']; + } else { + // developer didn't specify this name in the form + // warn him about it with a faked upload + return new HTTP_Upload_File( + '_error', null, + null, null, + null, 'DEV_NO_DEF_FILE', + $this->lang); + } + } + return $this->files; + } + + /** + * Creates the list of the uploaded file + * + * @return array of HTTP_Upload_File objects for every file + */ + function &_buildFiles() + { + // Form method check + if (!isset($this->content_type) || + strpos($this->content_type, 'multipart/form-data') !== 0) + { + return $this->raiseError('BAD_FORM'); + } + // In 4.1 $_FILES isn't initialized when no uploads + // XXX (cox) afaik, in >= 4.1 and <= 4.3 only + if (function_exists('version_compare') && + version_compare(phpversion(), '4.1', 'ge')) + { + $error = $this->isMissing(); + if (PEAR::isError($error)) { + return $error; + } + } + + // map error codes from 4.2.0 $_FILES['userfile']['error'] + if (function_exists('version_compare') && + version_compare(phpversion(), '4.2.0', 'ge')) { + $uploadError = array( + 1 => 'TOO_LARGE', + 2 => 'TOO_LARGE', + 3 => 'PARTIAL', + 4 => 'NO_USER_FILE' + ); + } + + + // Parse $_FILES (or $HTTP_POST_FILES) + $files = array(); + foreach ($this->post_files as $userfile => $value) { + if (is_array($value['name'])) { + foreach ($value['name'] as $key => $val) { + $err = $value['error'][$key]; + if (isset($err) && $err !== 0 && isset($uploadError[$err])) { + $error = $uploadError[$err]; + } else { + $error = null; + } + $name = basename($value['name'][$key]); + $tmp_name = $value['tmp_name'][$key]; + $size = $value['size'][$key]; + $type = $value['type'][$key]; + $formname = $userfile . "[$key]"; + $files[$formname] = new HTTP_Upload_File($name, $tmp_name, + $formname, $type, $size, $error, $this->lang, $this->_chmod); + } + // One file + } else { + $err = $value['error']; + if (isset($err) && $err !== 0 && isset($uploadError[$err])) { + $error = $uploadError[$err]; + } else { + $error = null; + } + $name = basename($value['name']); + $tmp_name = $value['tmp_name']; + $size = $value['size']; + $type = $value['type']; + $formname = $userfile; + $files[$formname] = new HTTP_Upload_File($name, $tmp_name, + $formname, $type, $size, $error, $this->lang, $this->_chmod); + } + } + return $files; + } + + /** + * Checks if the user submited or not some file + * + * @return mixed False when are files or PEAR_Error when no files + * @access public + * @see Read the note in the source code about this function + */ + function isMissing() + { + if (count($this->post_files) < 1) { + return $this->raiseError('NO_USER_FILE'); + } + //we also check if at least one file has more than 0 bytes :) + $files = array(); + $size = 0; + foreach ($this->post_files as $userfile => $value) { + if (is_array($value['name'])) { + foreach ($value['name'] as $key => $val) { + $size += $value['size'][$key]; + } + } else { //one file + $size = $value['size']; + } + } + if ($size == 0) { + $this->raiseError('NO_USER_FILE'); + } + return false; + } + + /** + * Sets the chmod to be used for uploaded files + * + * @param int Desired mode + */ + function setChmod($mode) + { + $this->_chmod = $mode; + } +} + +/** + * This class provides functions to work with the uploaded file + * + * @author Tomas V.V.Cox + * @see http://vulcanonet.com/soft/index.php?pack=uploader + * @package HTTP_Upload + * @category HTTP + * @access public + */ +class HTTP_Upload_File extends HTTP_Upload_Error +{ + /** + * If the random seed was initialized before or not + * @var boolean; + */ + var $_seeded = 0; + + /** + * Assoc array with file properties + * @var array + */ + var $upload = array(); + + /** + * If user haven't selected a mode, by default 'safe' will be used + * @var boolean + */ + var $mode_name_selected = false; + + /** + * It's a common security risk in pages who has the upload dir + * under the document root (remember the hack of the Apache web?) + * + * @var array + * @access private + * @see HTTP_Upload_File::setValidExtensions() + */ + var $_extensions_check = array('php', 'phtm', 'phtml', 'php3', 'inc'); + + /** + * @see HTTP_Upload_File::setValidExtensions() + * @var string + * @access private + */ + var $_extensions_mode = 'deny'; + + /** + * Contains the desired chmod for uploaded files + * @var int + * @access private + */ + var $_chmod = HTTP_UPLOAD_DEFAULT_CHMOD; + + /** + * Constructor + * + * @param string $name destination file name + * @param string $tmp temp file name + * @param string $formname name of the form + * @param string $type Mime type of the file + * @param string $size size of the file + * @param string $error error on upload + * @param string $lang used language for errormessages + * @access public + */ + function HTTP_Upload_File($name = null, $tmp = null, $formname = null, + $type = null, $size = null, $error = null, + $lang = null, $chmod = HTTP_UPLOAD_DEFAULT_CHMOD) + { + $this->HTTP_Upload_Error($lang); + $ext = null; + + if (empty($name) || $size == 0) { + $error = 'NO_USER_FILE'; + } elseif ($tmp == 'none') { + $error = 'TOO_LARGE'; + } else { + // strpos needed to detect files without extension + if (($pos = strrpos($name, '.')) !== false) { + $ext = substr($name, $pos + 1); + } + } + + if (function_exists('version_compare') && + version_compare(phpversion(), '4.1', 'ge')) { + if (isset($_POST['MAX_FILE_SIZE']) && + $size > $_POST['MAX_FILE_SIZE']) { + $error = 'TOO_LARGE'; + } + } else { + global $HTTP_POST_VARS; + if (isset($HTTP_POST_VARS['MAX_FILE_SIZE']) && + $size > $HTTP_POST_VARS['MAX_FILE_SIZE']) { + $error = 'TOO_LARGE'; + } + } + + $this->upload = array( + 'real' => $name, + 'name' => $name, + 'form_name' => $formname, + 'ext' => $ext, + 'tmp_name' => $tmp, + 'size' => $size, + 'type' => $type, + 'error' => $error + ); + + $this->_chmod = $chmod; + } + + /** + * Sets the name of the destination file + * + * @param string $mode A valid mode: 'uniq', 'safe' or 'real' or a file name + * @param string $prepend A string to prepend to the name + * @param string $append A string to append to the name + * + * @return string The modified name of the destination file + * @access public + */ + function setName($mode, $prepend = null, $append = null) + { + switch ($mode) { + case 'uniq': + $name = $this->nameToUniq(); + $this->upload['ext'] = $this->nameToSafe($this->upload['ext'], 10); + $name .= '.' . $this->upload['ext']; + break; + case 'safe': + $name = $this->nameToSafe($this->upload['real']); + if (($pos = strrpos($name, '.')) !== false) { + $this->upload['ext'] = substr($name, $pos + 1); + } else { + $this->upload['ext'] = ''; + } + break; + case 'real': + $name = $this->upload['real']; + break; + default: + $name = $mode; + } + $this->upload['name'] = $prepend . $name . $append; + $this->mode_name_selected = true; + return $this->upload['name']; + } + + /** + * Unique file names in the form: 9022210413b75410c28bef.html + * @see HTTP_Upload_File::setName() + */ + function nameToUniq() + { + if (! $this->_seeded) { + srand((double) microtime() * 1000000); + $this->_seeded = 1; + } + $uniq = uniqid(rand()); + return $uniq; + } + + /** + * Format a file name to be safe + * + * @param string $file The string file name + * @param int $maxlen Maximun permited string lenght + * @return string Formatted file name + * @see HTTP_Upload_File::setName() + */ + function nameToSafe($name, $maxlen=250) + { + $noalpha = 'ÁÉÍÓÚÝáéíóúýÂÊÎÔÛâêîôûÀÈÌÒÙàèìòùÄËÏÖÜäëïöüÿÃãÕõÅåÑñÇç@°ºª'; + $alpha = 'AEIOUYaeiouyAEIOUaeiouAEIOUaeiouAEIOUaeiouyAaOoAaNnCcaooa'; + + $name = substr($name, 0, $maxlen); + $name = strtr($name, $noalpha, $alpha); + // not permitted chars are replaced with "_" + return preg_replace('/[^a-zA-Z0-9,._\+\()\-]/', '_', $name); + } + + /** + * The upload was valid + * + * @return bool If the file was submitted correctly + * @access public + */ + function isValid() + { + if ($this->upload['error'] === null) { + return true; + } + return false; + } + + /** + * User haven't submit a file + * + * @return bool If the user submitted a file or not + * @access public + */ + function isMissing() + { + if ($this->upload['error'] == 'NO_USER_FILE') { + return true; + } + return false; + } + + /** + * Some error occured during upload (most common due a file size problem, + * like max size exceeded or 0 bytes long). + * @return bool If there were errors submitting the file (probably + * because the file excess the max permitted file size) + * @access public + */ + function isError() + { + if (in_array($this->upload['error'], array('TOO_LARGE', 'BAD_FORM','DEV_NO_DEF_FILE'))) { + return true; + } + return false; + } + + /** + * Moves the uploaded file to its destination directory. + * + * @param string $dir_dest Destination directory + * @param bool $overwrite Overwrite if destination file exists? + * @return mixed True on success or Pear_Error object on error + * @access public + */ + function moveTo($dir_dest, $overwrite = true) + { + if (!$this->isValid()) { + return $this->raiseError($this->upload['error']); + } + + //Valid extensions check + if (!$this->_evalValidExtensions()) { + return $this->raiseError('NOT_ALLOWED_EXTENSION'); + } + + $err_code = $this->_chk_dir_dest($dir_dest); + if ($err_code !== false) { + return $this->raiseError($err_code); + } + // Use 'safe' mode by default if no other was selected + if (!$this->mode_name_selected) { + $this->setName('safe'); + } + + $name_dest = $dir_dest . DIRECTORY_SEPARATOR . $this->upload['name']; + + if (@is_file($name_dest)) { + if ($overwrite !== true) { + return $this->raiseError('FILE_EXISTS'); + } elseif (!is_writable($name_dest)) { + return $this->raiseError('CANNOT_OVERWRITE'); + } + } + + // copy the file and let php clean the tmp + if (!@move_uploaded_file($this->upload['tmp_name'], $name_dest)) { + return $this->raiseError('E_FAIL_MOVE'); + } + @chmod($name_dest, $this->_chmod); + return $this->getProp('name'); + } + + /** + * Check for a valid destination dir + * + * @param string $dir_dest Destination dir + * @return mixed False on no errors or error code on error + */ + function _chk_dir_dest($dir_dest) + { + if (!$dir_dest) { + return 'MISSING_DIR'; + } + if (!@is_dir ($dir_dest)) { + return 'IS_NOT_DIR'; + } + if (!is_writeable ($dir_dest)) { + return 'NO_WRITE_PERMS'; + } + return false; + } + /** + * Retrive properties of the uploaded file + * @param string $name The property name. When null an assoc array with + * all the properties will be returned + * @return mixed A string or array + * @see HTTP_Upload_File::HTTP_Upload_File() + * @access public + */ + function getProp($name = null) + { + if ($name === null) { + return $this->upload; + } + return $this->upload[$name]; + } + + /** + * Returns a error message, if a error occured + * (deprecated) Use getMessage() instead + * @return string a Error message + * @access public + */ + function errorMsg() + { + return $this->errorCode($this->upload['error']); + } + + /** + * Returns a error message, if a error occured + * @return string a Error message + * @access public + */ + function getMessage() + { + return $this->errorCode($this->upload['error']); + } + + /** + * Function to restrict the valid extensions on file uploads + * + * @param array $exts File extensions to validate + * @param string $mode The type of validation: + * 1) 'deny' Will deny only the supplied extensions + * 2) 'accept' Will accept only the supplied extensions + * as valid + * @access public + */ + function setValidExtensions($exts, $mode = 'deny') + { + $this->_extensions_check = $exts; + $this->_extensions_mode = $mode; + } + + /** + * Evaluates the validity of the extensions set by setValidExtensions + * + * @return bool False on non valid extension, true if they are valid + * @access private + */ + function _evalValidExtensions() + { + $exts = $this->_extensions_check; + settype($exts, 'array'); + if ($this->_extensions_mode == 'deny') { + if (in_array($this->getProp('ext'), $exts)) { + return false; + } + // mode == 'accept' + } else { + if (!in_array($this->getProp('ext'), $exts)) { + return false; + } + } + return true; + } +} +?> \ No newline at end of file diff --git a/Image/Text.php b/Image/Text.php new file mode 100644 index 0000000..9e66a61 --- /dev/null +++ b/Image/Text.php @@ -0,0 +1,1261 @@ + '#0d54e2', + * 1 => '#e8ce7a', + * 2 => '#7ae8ad' + * ); + * + * $text = "EXTERIOR: DAGOBAH -- DAY\nWith Yoda\nstrapped to\n\nhis back, Luke climbs up one of the many thick vines that grow in the swamp until he reaches the Dagobah statistics lab. Panting heavily, he continues his exercises -- grepping, installing new packages, logging in as root, and writing replacements for two-year-old shell scripts in PHP.\nYODA: Code! Yes. A programmer's strength flows from code maintainability. But beware of Perl. Terse syntax... more than one way to do it... default variables. The dark side of code maintainability are they. Easily they flow, quick to join you when code you write. If once you start down the dark path, forever will it dominate your destiny, consume you it will.\nLUKE: Is Perl better than PHP?\nYODA: No... no... no. Orderless, dirtier, more seductive.\nLUKE: But how will I know why PHP is better than Perl?\nYODA: You will know. When your code you try to read six months from now..."; + * + * $options = array( + * 'canvas' => array('width'=> 600,'height'=> 600), // Generate a new image 600x600 pixel + * 'cx' => 300, // Set center to the middle of the canvas + * 'cy' => 300, + * 'width' => 300, // Set text box size + * 'height' => 300, + * 'line_spacing' => 1, // Normal linespacing + * 'angle' => 45, // Text rotated by 45 + * 'color' => $colors, // Predefined colors + * 'background_color' => '#FF0000', //red background + * 'max_lines' => 100, // Maximum lines to render + * 'min_font_size' => 2, // Minimal/Maximal font size (for automeasurize) + * 'max_font_size' => 50, + * 'font_path' => './', // Settings for the font file + * 'font_file' => 'Vera.ttf', + * 'antialias' => true, // Antialiase font rendering + * 'halign' => IMAGE_TEXT_ALIGN_RIGHT, // Alignment to the right and middle + * 'valign' => IMAGE_TEXT_ALIGN_MIDDLE + * ); + * + * // Generate a new Image_Text object + * $itext = new Image_Text($text, $options); + * + * // Initialize and check the settings + * $itext->init(); + + * // Automatically determine optimal font size + * $itext->autoMeasurize(); + * + * // Render the image + * $itext->render(); + * + * // Display it + * $itext->display(); + * + * -------- End example -------- + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Image + * @package Text + * @author Tobias Schlitt + * @copyright 1997-2005 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Text.php,v 1.32 2007/04/16 09:52:34 cweiske Exp $ + * @link http://pear.php.net/package/Net_FTP2 + * @since File available since Release 0.0.1 + */ + +/** + * Require PEAR file for error handling. + */ +require_once 'PEAR.php'; + +/** + * Regex to match HTML style hex triples. + */ +define("IMAGE_TEXT_REGEX_HTMLCOLOR", "/^[#|]([a-f0-9]{2})?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i", true); + +/** + * Defines horizontal alignment to the left of the text box. (This is standard.) + */ +define("IMAGE_TEXT_ALIGN_LEFT", "left", true); + +/** + * Defines horizontal alignment to the center of the text box. + */ +define("IMAGE_TEXT_ALIGN_RIGHT", "right", true); + +/** + * Defines horizontal alignment to the center of the text box. + */ +define("IMAGE_TEXT_ALIGN_CENTER", "center", true); + +/** + * Defines vertical alignment to the to the top of the text box. (This is standard.) + */ +define("IMAGE_TEXT_ALIGN_TOP", "top", true); + +/** + * Defines vertical alignment to the to the middle of the text box. + */ +define("IMAGE_TEXT_ALIGN_MIDDLE", "middle", true); + +/** + * Defines vertical alignment to the to the bottom of the text box. + */ +define("IMAGE_TEXT_ALIGN_BOTTOM", "bottom", true); + +/** + * TODO: This constant is useless until now, since justified alignment does not work yet + */ +define("IMAGE_TEXT_ALIGN_JUSTIFY", "justify", true); + +/** + * Image_Text - Advanced text maipulations in images + * + * Image_Text provides advanced text manipulation facilities for GD2 + * image generation with PHP. Simply add text clippings to your images, + * let the class automatically determine lines, rotate text boxes around + * their center or top left corner. These are only a couple of features + * Image_Text provides. + * + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @category Image + * @package Text + * @author Tobias Schlitt + * @copyright 1997-2005 The PHP Group + * @version Release: @package_version@ + * @link http://pear.php.net/package/Net_FTP + * @since 0.0.1 + * @access public + */ +class Image_Text { + + /** + * Options array. these options can be set through the constructor or the set() method. + * + * Possible options to set are: + *
+     *
+     *      'x'                 | This sets the top left coordinates (using x/y) or the center point
+     *      'y'                 | coordinates (using cx/cy) for your text box. The values from
+     *      'cx'                | cx/cy will overwrite x/y.
+     *      'cy'                |
+     *
+     *      'canvas'            | You can set different values as a canvas:
+     *                          |   - A gd image resource.
+     *                          |   - An array with 'width' and 'height'.
+     *                          |   - Nothing (the canvas will be measured after the real text size).
+     *
+     *      'antialias'         | This is usually true. Set it to false to switch antialiasing off.
+     *
+     *      'width'             | The width and height for your text box.
+     *      'height'            |
+     *
+     *      'halign'            | Alignment of your text inside the textbox. Use alignmnet constants to define
+     *      'valign'            | vertical and horizontal alignment.
+     *
+     *      'angle'             | The angle to rotate your text box.
+     *
+     *      'color'             | An array of color values. Colors will be rotated in the mode you choose (linewise
+     *                          | or paragraphwise). Can be in the following formats:
+     *                          |   - String representing HTML style hex couples (+ unusual alpha couple in the first place, optional).
+     *                          |   - Array of int values using 'r', 'g', 'b' and optionally 'a' as keys.
+     *
+     *      'color_mode'        | The color rotation mode for your color sets. Does only apply if you
+     *                          | defined multiple colors. Use 'line' or 'paragraph'.
+     *
+     *      'background_color'  | defines the background color. NULL sets it transparent
+     *      'enable_alpha'      | if alpha channel should be enabled. Automatically
+     *                          | enabled when background_color is set to NULL
+     *
+     *      'font_path'         | Location of the font to use. The path only gives the directory path (ending with a /).
+     *      'font_file'         | The fontfile is given in the 'font_file' option.
+     *
+     *      'font_size'         | The font size to render text in (will be overwriten, if you use automeasurize).
+     *
+     *      'line_spacing'      | Measure for the line spacing to use. Default is 0.5.
+     *
+     *      'min_font_size'     | Automeasurize settings. Try to keep this area as small as possible to get better
+     *      'max_font_size'     | performance.
+     *
+     *      'image_type'        | The type of image (use image type constants). Is default set to PNG.
+     *
+     *      'dest_file'         | The destination to (optionally) save your file.
+     * 
+ * + * @access public + * @var array + * @see Image_Text::Image_Text(), Image_Text::set() + */ + + var $options = array( + // orientation + 'x' => 0, + 'y' => 0, + + // surface + 'canvas' => null, + 'antialias' => true, + + // text clipping + 'width' => 0, + 'height' => 0, + + // text alignment inside the clipping + 'halign' => IMAGE_TEXT_ALIGN_LEFT, + 'valign' => IMAGE_TEXT_ALIGN_TOP, + + // angle to rotate the text clipping + 'angle' => 0, + + // color settings + 'color' => array( '#000000' ), + + 'color_mode' => 'line', + + 'background_color' => '#000000', + 'enable_alpha' => false, + + // font settings + 'font_path' => "./", + 'font_file' => null, + 'font_size' => 2, + 'line_spacing' => 0.5, + + // automasurizing settings + 'min_font_size' => 1, + 'max_font_size' => 100, + + //max. lines to render + 'max_lines' => 100, + + // misc settings + 'image_type' => IMAGETYPE_PNG, + 'dest_file' => '' + ); + + /** + * Contains option names, which can cause re-initialization force. + * + * @var array + * @access private + */ + + var $_reInits = array('width', 'height', 'canvas', 'angle', 'font_file', 'font_path', 'font_size'); + + /** + * The text you want to render. + * + * @access private + * @var string + */ + + var $_text; + + /** + * Resource ID of the image canvas. + * + * @access private + * @var ressource + */ + + var $_img; + + /** + * Tokens (each word). + * + * @access private + * @var array + */ + + var $_tokens = array(); + + /** + * Fullpath to the font. + * + * @access private + * @var string + */ + + var $_font; + + /** + * Contains the bbox of each rendered lines. + * + * @access private + * @var array + */ + + var $bbox = array(); + + /** + * Defines in which mode the canvas has be set. + * + * @access private + * @var array + */ + + var $_mode = ''; + + /** + * Color indeces returned by imagecolorallocatealpha. + * + * @access public + * @var array + */ + + var $colors = array(); + + /** + * Width and height of the (rendered) text. + * + * @access private + * @var array + */ + + var $_realTextSize = array('width' => false, 'height' => false); + + /** + * Measurized lines. + * + * @access private + * @var array + */ + + var $_lines = false; + + /** + * Fontsize for which the last measuring process was done. + * + * @access private + * @var array + */ + + var $_measurizedSize = false; + + /** + * Is the text object initialized? + * + * @access private + * @var bool + */ + + var $_init = false; + + /** + * Constructor + * + * Set the text and options. This initializes a new Image_Text object. You must set your text + * here. Optinally you can set all options here using the $options parameter. If you finished switching + * all options you have to call the init() method first befor doing anything further! See Image_Text::set() + * for further information. + * + * @param string $text Text to print. + * @param array $options Options. + * @access public + * @see Image_Text::set(), Image_Text::construct(), Image_Text::init() + */ + + function Image_Text($text, $options = null) + { + $this->set('text', $text); + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * Construct and initialize an Image_Text in one step. + * This method is called statically and creates plus initializes an Image_Text object. + * Beware: You will have to recall init() if you set an option afterwards manually. + * + * @param string $text Text to print. + * @param array $options Options. + * @access public + * @static + * @see Image_Text::set(), Image_Text::Image_Text(), Image_Text::init() + */ + + function &construct ( $text, $options ) { + $itext = new Image_Text($text, $options); + $res = $itext->init(); + if (PEAR::isError($res)) { + return $res; + } + return $itext; + } + + /** + * Set options + * + * Set a single or multiple options. It may happen that you have to reinitialize the Image_Text + * object after changing options. For possible options, please take a look at the class options + * array! + * + * + * @param mixed $option A single option name or the options array. + * @param mixed $value Option value if $option is string. + * @return bool True on success, otherwise PEAR::Error. + * @access public + * @see Image_Text::Image_Text() + */ + + function set($option, $value=null) + { + $reInits = array_flip($this->_reInits); + if (!is_array($option)) { + if (!isset($value)) { + return PEAR::raiseError('No value given.'); + } + $option = array($option => $value); + } + foreach ($option as $opt => $val) { + switch ($opt) { + case 'color': + $this->setColors($val); + break; + case 'text': + if (is_array($val)) { + $this->_text = implode('\n', $val); + } else { + $this->_text = $val; + } + break; + default: + $this->options[$opt] = $val; + break; + } + if (isset($reInits[$opt])) { + $this->_init = false; + } + } + return true; + } + + /** + * Set the color-set + * + * Using this method you can set multiple colors for your text. + * Use a simple numeric array to determine their order and give + * it to this function. Multiple colors will be + * cycled by the options specified 'color_mode' option. The given array + * will overwrite the existing color settings! + * + * The following colors syntaxes are understood by this method: + * - "#ffff00" hexadecimal format (HTML style), with and without #. + * - "#08ffff00" hexadecimal format (HTML style) with alpha channel (08), with and without #. + * - array with 'r','g','b' and (optionally) 'a' keys, using int values. + * - a GD color special color (tiled,...). + * + * A single color or an array of colors are allowed here. + * + * @param mixed $colors Single color or array of colors. + * @return bool True on success, otherwise PEAR::Error. + * @access public + * @see Image_Text::setColor(), Image_Text::set() + */ + + function setColors($colors) + { + $i = 0; + if (is_array($colors) && + (is_string($colors[0]) || is_array($colors[0])) + ) { + foreach ($colors as $color) { + $res = $this->setColor($color,$i++); + if (PEAR::isError($res)) { + return $res; + } + } + } else { + return $this->setColor($colors, $i); + } + return true; + } + + /** + * Set a color + * + * This method is used to set a color at a specific color ID inside the + * color cycle. + * + * The following colors syntaxes are understood by this method: + * - "#ffff00" hexadecimal format (HTML style), with and without #. + * - "#08ffff00" hexadecimal format (HTML style) with alpha channel (08), with and without #. + * - array with 'r','g','b' and (optionally) 'a' keys, using int values. + * + * @param mixed $color Color value. + * @param mixed $id ID (in the color array) to set color to. + * @return bool True on success, otherwise PEAR::Error. + * @access public + * @see Image_Text::setColors(), Image_Text::set() + */ + + function setColor($color, $id=0) + { + if(is_array($color)) { + if (isset($color['r']) && isset($color['g']) && isset($color['b'])) { + $color['a'] = isset($color['a']) ? $color['a'] : 0; + $this->options['colors'][$id] = $color; + } else if (isset($color[0]) && isset($color[1]) && isset($color[2])) { + $color['r'] = $color[0]; + $color['g'] = $color[1]; + $color['b'] = $color[2]; + $color['a'] = isset($color[3]) ? $color[3] : 0; + $this->options['colors'][$id] = $color; + } else { + return PEAR::raiseError('Use keys 1,2,3 (optionally) 4 or r,g,b and (optionally) a.'); + } + } elseif (is_string($color)) { + $color = $this->_convertString2RGB($color); + if ($color) { + $this->options['color'][$id] = $color; + } else { + return PEAR::raiseError('Invalid color.'); + } + } + if ($this->_img) { + $aaFactor = ($this->options['antialias']) ? 1 : -1; + if (function_exists('imagecolorallocatealpha') && isset($color['a'])) { + $this->colors[$id] = $aaFactor * imagecolorallocatealpha($this->_img, + $color['r'],$color['g'],$color['b'],$color['a']); + } else { + $this->colors[$id] = $aaFactor * imagecolorallocate($this->_img, + $color['r'],$color['g'],$color['b']); + } + } + return true; + } + + /** + * Initialiaze the Image_Text object. + * + * This method has to be called after setting the options for your Image_Text object. + * It initializes the canvas, normalizes some data and checks important options. + * Be shure to check the initialization after you switched some options. The + * set() method may force you to reinitialize the object. + * + * @access public + * @return bool True on success, otherwise PEAR::Error. + * @see Image_Text::set() + */ + + function init() + { + // Does the fontfile exist and is readable? + // todo: with some versions of the GD-library it's also possible to leave font_path empty, add strip ".ttf" from + // the fontname; the fontfile will then be automatically searched for in library-defined directories + // however this does not yet work if at this point we check for the existance of the fontfile + $font_file = rtrim($this->options['font_path'], '/\\'); + $font_file.= (OS_WINDOWS) ? '\\' : '/'; + $font_file.= $this->options['font_file']; + $font_file = realpath($font_file); + if (empty($font_file) || !is_file($font_file) || !is_readable($font_file)) { + return PEAR::raiseError('Fontfile not found or not readable.'); + } else { + $this->_font = $font_file; + } + + // Is the font size to small? + if ($this->options['width'] < 1) { + return PEAR::raiseError('Width too small. Has to be > 1.'); + } + + // Check and create canvas + + switch (true) { + case (empty($this->options['canvas'])): + + // Create new image from width && height of the clipping + $this->_img = imagecreatetruecolor( + $this->options['width'], $this->options['height']); + if (!$this->_img) { + return PEAR::raiseError('Could not create image canvas.'); + } + break; + + case (is_resource($this->options['canvas']) && + get_resource_type($this->options['canvas'])=='gd'): + // The canvas is an image resource + $this->_img = $this->options['canvas']; + break; + + case (is_array($this->options['canvas']) && + isset($this->options['canvas']['width']) && + isset($this->options['canvas']['height'])): + + // Canvas must be a width and height measure + $this->_img = imagecreatetruecolor( + $this->options['canvas']['width'], + $this->options['canvas']['height'] + ); + break; + + + case (is_array($this->options['canvas']) && + isset($this->options['canvas']['size']) && + ($this->options['canvas']['size'] = 'auto')): + + case (is_string($this->options['canvas']) && + ($this->options['canvas'] = 'auto')): + $this->_mode = 'auto'; + break; + + default: + return PEAR::raiseError('Could not create image canvas.'); + + } + + + + if ($this->_img) { + $this->options['canvas'] = array(); + $this->options['canvas']['width'] = imagesx($this->_img); + $this->options['canvas']['height'] = imagesy($this->_img); + } + + if ($this->options['enable_alpha']) { + imagesavealpha($this->_img, true); + imagealphablending($this->_img, false); + } + + if ($this->options['background_color'] === null) { + $this->options['enable_alpha'] = true; + imagesavealpha($this->_img, true); + imagealphablending($this->_img, false); + $colBg = imagecolorallocatealpha($this->_img, 255, 255, 255, 127); + } else { + $arBg = $this->_convertString2RGB($this->options['background_color']); + if ($arBg === false) { + return PEAR::raiseError('Background color is invalid.'); + } + $colBg = imagecolorallocatealpha($this->_img, $arBg['r'], $arBg['g'], $arBg['b'], $arBg['a']); + } + imagefilledrectangle( + $this->_img, + 0, 0, + $this->options['canvas']['width'] - 1, $this->options['canvas']['height'] - 1, + $colBg + ); + + + // Save and repair angle + $angle = $this->options['angle']; + while ($angle < 0) { + $angle += 360; + } + if ($angle > 359) { + $angle = $angle % 360; + } + $this->options['angle'] = $angle; + + // Set the color values + $res = $this->setColors($this->options['color']); + if (PEAR::isError($res)) { + return $res; + } + + $this->_lines = null; + + // Initialization is complete + $this->_init = true; + return true; + } + + /** + * Auto measurize text + * + * Automatically determines the greatest possible font size to + * fit the text into the text box. This method may be very resource + * intensive on your webserver. A good tweaking point are the $start + * and $end parameters, which specify the range of font sizes to search + * through. Anyway, the results should be cached if possible. You can + * optionally set $start and $end here as a parameter or the settings of + * the options array are used. + * + * @access public + * @param int $start Fontsize to start testing with. + * @param int $end Fontsize to end testing with. + * @return int Fontsize measured or PEAR::Error. + * @see Image_Text::measurize() + */ + + function autoMeasurize($start=false, $end=false) + { + if (!$this->_init) { + return PEAR::raiseError('Not initialized. Call ->init() first!'); + } + + $start = (empty($start)) ? $this->options['min_font_size'] : $start; + $end = (empty($end)) ? $this->options['max_font_size'] : $end; + + $res = false; + // Run through all possible font sizes until a measurize fails + // Not the optimal way. This can be tweaked! + for ($i = $start; $i <= $end; $i++) { + $this->options['font_size'] = $i; + $res = $this->measurize(); + + if ($res === false) { + if ($start == $i) { + $this->options['font_size'] = -1; + return PEAR::raiseError("No possible font size found"); + } + $this->options['font_size'] -= 1; + $this->_measurizedSize = $this->options['font_size']; + break; + } + // Always the last couple of lines is stored here. + $this->_lines = $res; + } + return $this->options['font_size']; + } + + /** + * Measurize text into the text box + * + * This method makes your text fit into the defined textbox by measurizing the + * lines for your given font-size. You can do this manually before rendering (or use + * even Image_Text::autoMeasurize()) or the renderer will do measurizing + * automatically. + * + * @access public + * @param bool $force Optionally, default is false, set true to force measurizing. + * @return array Array of measured lines or PEAR::Error. + * @see Image_Text::autoMeasurize() + */ + + function measurize($force=false) + { + if (!$this->_init) { + return PEAR::raiseError('Not initialized. Call ->init() first!'); + } + $this->_processText(); + + // Precaching options + $font = $this->_font; + $size = $this->options['font_size']; + + $line_spacing = $this->options['line_spacing']; + $space = (1 + $this->options['line_spacing']) * $this->options['font_size']; + + $max_lines = (int)$this->options['max_lines']; + + if (($max_lines<1) && !$force) { + return false; + } + + $block_width = $this->options['width']; + $block_height = $this->options['height']; + + $colors_cnt = sizeof($this->colors); + $c = $this->colors[0]; + + $text_line = ''; + + $lines_cnt = 0; + $tokens_cnt = sizeof($this->_tokens); + + $lines = array(); + + $text_height = 0; + $text_width = 0; + + $i = 0; + $para_cnt = 0; + + $beginning_of_line = true; + + // Run through tokens and order them in lines + foreach($this->_tokens as $token) { + // Handle new paragraphs + if ($token=="\n") { + $bounds = imagettfbbox($size, 0, $font, $text_line); + if ((++$lines_cnt>=$max_lines) && !$force) { + return false; + } + if ($this->options['color_mode']=='paragraph') { + $c = $this->colors[$para_cnt%$colors_cnt]; + $i++; + } else { + $c = $this->colors[$i++%$colors_cnt]; + } + $lines[] = array( + 'string' => $text_line, + 'width' => $bounds[2]-$bounds[0], + 'height' => $bounds[1]-$bounds[7], + 'bottom_margin' => $bounds[1], + 'left_margin' => $bounds[0], + 'color' => $c + ); + $text_width = max($text_width, ($bounds[2]-$bounds[0])); + $text_height += (int)$space; + if (($text_height > $block_height) && !$force) { + return false; + } + $para_cnt++; + $text_line = ''; + $beginning_of_line = true; + continue; + } + + // Usual lining up + + if ($beginning_of_line) { + $text_line = ''; + $text_line_next = $token; + $beginning_of_line = false; + } else { + $text_line_next = $text_line.' '.$token; + } + $bounds = imagettfbbox($size, 0, $font, $text_line_next); + $prev_width = isset($prev_width)?$width:0; + $width = $bounds[2]-$bounds[0]; + + // Handling of automatic new lines + if ($width>$block_width) { + if ((++$lines_cnt>=$max_lines) && !$force) { + return false; + } + if ($this->options['color_mode']=='line') { + $c = $this->colors[$i++%$colors_cnt]; + } else { + $c = $this->colors[$para_cnt%$colors_cnt]; + $i++; + } + + $lines[] = array( + 'string' => $text_line, + 'width' => $prev_width, + 'height' => $bounds[1]-$bounds[7], + 'bottom_margin' => $bounds[1], + 'left_margin' => $bounds[0], + 'color' => $c + ); + $text_width = max($text_width, ($bounds[2]-$bounds[0])); + $text_height += (int)$space; + if (($text_height > $block_height) && !$force) { + return false; + } + + $text_line = $token; + $bounds = imagettfbbox($size, 0, $font, $text_line); + $width = $bounds[2]-$bounds[0]; + $beginning_of_line = false; + } else { + $text_line = $text_line_next; + } + } + // Store remaining line + $bounds = imagettfbbox($size, 0, $font,$text_line); + if ($this->options['color_mode']=='line') { + $c = $this->colors[$i++%$colors_cnt]; + } else { + $c = $this->colors[$para_cnt%$colors_cnt]; + $i++; + } + $lines[] = array( + 'string'=> $text_line, + 'width' => $bounds[2]-$bounds[0], + 'height'=> $bounds[1]-$bounds[7], + 'bottom_margin' => $bounds[1], + 'left_margin' => $bounds[0], + 'color' => $c + ); + + // add last line height, but without the line-spacing + $text_height += $this->options['font_size']; + + $text_width = max($text_width, ($bounds[2]-$bounds[0])); + + if (($text_height > $block_height) && !$force) { + return false; + } + + $this->_realTextSize = array('width' => $text_width, 'height' => $text_height); + $this->_measurizedSize = $this->options['font_size']; + + return $lines; + } + + /** + * Render the text in the canvas using the given options. + * + * This renders the measurized text or automatically measures it first. The $force parameter + * can be used to switch of measurizing problems (this may cause your text being rendered + * outside a given text box or destroy your image completely). + * + * @access public + * @param bool $force Optional, initially false, set true to silence measurize errors. + * @return bool True on success, otherwise PEAR::Error. + */ + + function render($force=false) + { + if (!$this->_init) { + return PEAR::raiseError('Not initialized. Call ->init() first!'); + } + + if (!$this->_tokens) { + $this->_processText(); + } + + if (empty($this->_lines) || ($this->_measurizedSize != $this->options['font_size'])) { + $this->_lines = $this->measurize( $force ); + } + $lines = $this->_lines; + + if (PEAR::isError($this->_lines)) { + return $this->_lines; + } + + if ($this->_mode === 'auto') { + $this->_img = imagecreatetruecolor( + $this->_realTextSize['width'], + $this->_realTextSize['height'] + ); + if (!$this->_img) { + return PEAR::raiseError('Could not create image cabvas.'); + } + $this->_mode = ''; + $this->setColors($this->_options['color']); + } + + $block_width = $this->options['width']; + $block_height = $this->options['height']; + + $max_lines = $this->options['max_lines']; + + $angle = $this->options['angle']; + $radians = round(deg2rad($angle), 3); + + $font = $this->_font; + $size = $this->options['font_size']; + + $line_spacing = $this->options['line_spacing']; + + $align = $this->options['halign']; + + $im = $this->_img; + + $offset = $this->_getOffset(); + + $start_x = $offset['x']; + $start_y = $offset['y']; + + $end_x = $start_x + $block_width; + $end_y = $start_y + $block_height; + + $sinR = sin($radians); + $cosR = cos($radians); + + switch ($this->options['valign']) { + case IMAGE_TEXT_ALIGN_TOP: + $valign_space = 0; + break; + case IMAGE_TEXT_ALIGN_MIDDLE: + $valign_space = ($this->options['height'] - $this->_realTextSize['height']) / 2; + break; + case IMAGE_TEXT_ALIGN_BOTTOM: + $valign_space = $this->options['height'] - $this->_realTextSize['height']; + break; + default: + $valign_space = 0; + } + + $space = (1 + $line_spacing) * $size; + + // Adjustment of align + translation of top-left-corner to bottom-left-corner of first line + $new_posx = $start_x + ($sinR * ($valign_space + $size)); + $new_posy = $start_y + ($cosR * ($valign_space + $size)); + + $lines_cnt = min($max_lines,sizeof($lines)); + + // Go thorugh lines for rendering + for($i=0; $i<$lines_cnt; $i++){ + + // Calc the new start X and Y (only for line>0) + // the distance between the line above is used + if($i > 0){ + $new_posx += $sinR * $space; + $new_posy += $cosR * $space; + } + + // Calc the position of the 1st letter. We can then get the left and bottom margins + // 'i' is really not the same than 'j' or 'g'. + $bottom_margin = $lines[$i]['bottom_margin']; + $left_margin = $lines[$i]['left_margin']; + $line_width = $lines[$i]['width']; + + // Calc the position using the block width, the current line width and obviously + // the angle. That gives us the offset to slide the line. + switch($align) { + case IMAGE_TEXT_ALIGN_LEFT: + $hyp = 0; + break; + case IMAGE_TEXT_ALIGN_RIGHT: + $hyp = $block_width - $line_width - $left_margin; + break; + case IMAGE_TEXT_ALIGN_CENTER: + $hyp = ($block_width-$line_width)/2 - $left_margin; + break; + default: + $hyp = 0; + break; + } + + $posx = $new_posx + $cosR * $hyp; + $posy = $new_posy - $sinR * $hyp; + + $c = $lines[$i]['color']; + + // Render textline + $bboxes[] = imagettftext ($im, $size, $angle, $posx, $posy, $c, $font, $lines[$i]['string']); + } + $this->bbox = $bboxes; + return true; + } + + /** + * Return the image ressource. + * + * Get the image canvas. + * + * @access public + * @return resource Used image resource + */ + + function &getImg() + { + return $this->_img; + } + + /** + * Display the image (send it to the browser). + * + * This will output the image to the users browser. You can use the standard IMAGETYPE_* + * constants to determine which image type will be generated. Optionally you can save your + * image to a destination you set in the options. + * + * @param bool $save Save or not the image on printout. + * @param bool $free Free the image on exit. + * @return bool True on success, otherwise PEAR::Error. + * @access public + * @see Image_Text::save() + */ + + function display($save=false, $free=false) + { + if (!headers_sent()) { + header("Content-type: " .image_type_to_mime_type($this->options['image_type'])); + } else { + PEAR::raiseError('Header already sent.'); + } + switch ($this->options['image_type']) { + case IMAGETYPE_PNG: + $imgout = 'imagepng'; + break; + case IMAGETYPE_JPEG: + $imgout = 'imagejpeg'; + break; + case IMAGETYPE_BMP: + $imgout = 'imagebmp'; + break; + default: + return PEAR::raiseError('Unsupported image type.'); + break; + } + if ($save) { + $imgout($this->_img); + $res = $this->save(); + if (PEAR::isError($res)) { + return $res; + } + } else { + $imgout($this->_img); + } + + if ($free) { + $res = imagedestroy($this->image); + if (!$res) { + PEAR::raiseError('Destroying image failed.'); + } + } + return true; + } + + /** + * Save image canvas. + * + * Saves the image to a given destination. You can leave out the destination file path, + * if you have the option for that set correctly. Saving is possible with the save() + * method, too. + * + * @param string $destFile The destination to save to (optional, uses options value else). + * @return bool True on success, otherwise PEAR::Error. + * @see Image_Text::display() + */ + + function save($dest_file=false) + { + if (!$dest_file) { + $dest_file = $this->options['dest_file']; + } + if (!$dest_file) { + return PEAR::raiseError("Invalid desitination file."); + } + + switch ($this->options['image_type']) { + case IMAGETYPE_PNG: + $imgout = 'imagepng'; + break; + case IMAGETYPE_JPEG: + $imgout = 'imagejpeg'; + break; + case IMAGETYPE_BMP: + $imgout = 'imagebmp'; + break; + default: + return PEAR::raiseError('Unsupported image type.'); + break; + } + + $res = $imgout($this->_img, $dest_file); + if (!$res) { + PEAR::raiseError('Saving file failed.'); + } + return true; + } + + /** + * Get completely translated offset for text rendering. + * + * Get completely translated offset for text rendering. Important + * for usage of center coords and angles + * + * @access private + * @return array Array of x/y coordinates. + */ + + function _getOffset() + { + // Presaving data + $width = $this->options['width']; + $height = $this->options['height']; + $angle = $this->options['angle']; + $x = $this->options['x']; + $y = $this->options['y']; + // Using center coordinates + if (!empty($this->options['cx']) && !empty($this->options['cy'])) { + $cx = $this->options['cx']; + $cy = $this->options['cy']; + // Calculation top left corner + $x = $cx - ($width / 2); + $y = $cy - ($height / 2); + // Calculating movement to keep the center point on himslf after rotation + if ($angle) { + $ang = deg2rad($angle); + // Vector from the top left cornern ponting to the middle point + $vA = array( ($cx - $x), ($cy - $y) ); + // Matrix to rotate vector + // sinus and cosinus + $sin = round(sin($ang), 14); + $cos = round(cos($ang), 14); + // matrix + $mRot = array( + $cos, (-$sin), + $sin, $cos + ); + // Multiply vector with matrix to get the rotated vector + // This results in the location of the center point after rotation + $vB = array ( + ($mRot[0] * $vA[0] + $mRot[2] * $vA[0]), + ($mRot[1] * $vA[1] + $mRot[3] * $vA[1]) + ); + // To get the movement vector, we subtract the original middle + $vC = array ( + ($vA[0] - $vB[0]), + ($vA[1] - $vB[1]) + ); + // Finally we move the top left corner coords there + $x += $vC[0]; + $y += $vC[1]; + } + } + return array ('x' => (int)round($x, 0), 'y' => (int)round($y, 0)); + } + + /** + * Convert a color to an array. + * + * The following colors syntax must be used: + * "#08ffff00" hexadecimal format with alpha channel (08) + * array with 'r','g','b','a'(optionnal) keys + * A GD color special color (tiled,...) + * Only one color is allowed + * If $id is given, the color index $id is used + * + * @param mixed $colors Array of colors. + * @param mixed $id Array of colors. + * @access private + */ + function _convertString2RGB($scolor) + { + if (preg_match(IMAGE_TEXT_REGEX_HTMLCOLOR, $scolor, $matches)) { + return array( + 'r' => hexdec($matches[2]), + 'g' => hexdec($matches[3]), + 'b' => hexdec($matches[4]), + 'a' => hexdec(!empty($matches[1])?$matches[1]:0), + ); + } + return false; + } + + /** + * Extract the tokens from the text. + * + * @access private + */ + function _processText() + { + if (!isset($this->_text)) { + return false; + } + $this->_tokens = array(); + + // Normalize linebreak to "\n" + $this->_text = preg_replace("[\r\n]", "\n", $this->_text); + + // Get each paragraph + $paras = explode("\n",$this->_text); + + // loop though the paragraphs + // and get each word (token) + foreach($paras as $para) { + $words = explode(' ',$para); + foreach($words as $word) { + $this->_tokens[] = $word; + } + // add a "\n" to mark the end of a paragraph + $this->_tokens[] = "\n"; + } + // we do not need an end paragraph as the last token + array_pop($this->_tokens); + } +} + + diff --git a/Log.php b/Log.php new file mode 100644 index 0000000..1460376 --- /dev/null +++ b/Log.php @@ -0,0 +1,824 @@ + + * @author Jon Parise + * @since Horde 1.3 + * @package Log + */ +class Log +{ + /** + * Indicates whether or not the log can been opened / connected. + * + * @var boolean + * @access private + */ + var $_opened = false; + + /** + * Instance-specific unique identification number. + * + * @var integer + * @access private + */ + var $_id = 0; + + /** + * The label that uniquely identifies this set of log messages. + * + * @var string + * @access private + */ + var $_ident = ''; + + /** + * The default priority to use when logging an event. + * + * @var integer + * @access private + */ + var $_priority = PEAR_LOG_INFO; + + /** + * The bitmask of allowed log levels. + * + * @var integer + * @access private + */ + var $_mask = PEAR_LOG_ALL; + + /** + * Holds all Log_observer objects that wish to be notified of new messages. + * + * @var array + * @access private + */ + var $_listeners = array(); + + /** + * Maps canonical format keys to position arguments for use in building + * "line format" strings. + * + * @var array + * @access private + */ + var $_formatMap = array('%{timestamp}' => '%1$s', + '%{ident}' => '%2$s', + '%{priority}' => '%3$s', + '%{message}' => '%4$s', + '%{file}' => '%5$s', + '%{line}' => '%6$s', + '%{function}' => '%7$s', + '%\{' => '%%{'); + + + /** + * Attempts to return a concrete Log instance of type $handler. + * + * @param string $handler The type of concrete Log subclass to return. + * Attempt to dynamically include the code for + * this subclass. Currently, valid values are + * 'console', 'syslog', 'sql', 'file', and 'mcal'. + * + * @param string $name The name of the actually log file, table, or + * other specific store to use. Defaults to an + * empty string, with which the subclass will + * attempt to do something intelligent. + * + * @param string $ident The identity reported to the log system. + * + * @param array $conf A hash containing any additional configuration + * information that a subclass might need. + * + * @param int $level Log messages up to and including this level. + * + * @return object Log The newly created concrete Log instance, or + * null on an error. + * @access public + * @since Log 1.0 + */ + function &factory($handler, $name = '', $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $handler = strtolower($handler); + $class = 'Log_' . $handler; + $classfile = 'Log/' . $handler . '.php'; + + /* + * Attempt to include our version of the named class, but don't treat + * a failure as fatal. The caller may have already included their own + * version of the named class. + */ + if (!class_exists($class)) { + include_once $classfile; + } + + /* If the class exists, return a new instance of it. */ + if (class_exists($class)) { + $obj = &new $class($name, $ident, $conf, $level); + return $obj; + } + + $null = null; + return $null; + } + + /** + * Attempts to return a reference to a concrete Log instance of type + * $handler, only creating a new instance if no log instance with the same + * parameters currently exists. + * + * You should use this if there are multiple places you might create a + * logger, you don't want to create multiple loggers, and you don't want to + * check for the existance of one each time. The singleton pattern does all + * the checking work for you. + * + * You MUST call this method with the $var = &Log::singleton() syntax. + * Without the ampersand (&) in front of the method name, you will not get + * a reference, you will get a copy. + * + * @param string $handler The type of concrete Log subclass to return. + * Attempt to dynamically include the code for + * this subclass. Currently, valid values are + * 'console', 'syslog', 'sql', 'file', and 'mcal'. + * + * @param string $name The name of the actually log file, table, or + * other specific store to use. Defaults to an + * empty string, with which the subclass will + * attempt to do something intelligent. + * + * @param string $ident The identity reported to the log system. + * + * @param array $conf A hash containing any additional configuration + * information that a subclass might need. + * + * @param int $level Log messages up to and including this level. + * + * @return object Log The newly created concrete Log instance, or + * null on an error. + * @access public + * @since Log 1.0 + */ + function &singleton($handler, $name = '', $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + static $instances; + if (!isset($instances)) $instances = array(); + + $signature = serialize(array($handler, $name, $ident, $conf, $level)); + if (!isset($instances[$signature])) { + $instances[$signature] = &Log::factory($handler, $name, $ident, + $conf, $level); + } + + return $instances[$signature]; + } + + /** + * Abstract implementation of the open() method. + * @since Log 1.0 + */ + function open() + { + return false; + } + + /** + * Abstract implementation of the close() method. + * @since Log 1.0 + */ + function close() + { + return false; + } + + /** + * Abstract implementation of the flush() method. + * @since Log 1.8.2 + */ + function flush() + { + return false; + } + + /** + * Abstract implementation of the log() method. + * @since Log 1.0 + */ + function log($message, $priority = null) + { + return false; + } + + /** + * A convenience function for logging a emergency event. It will log a + * message at the PEAR_LOG_EMERG log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function emerg($message) + { + return $this->log($message, PEAR_LOG_EMERG); + } + + /** + * A convenience function for logging an alert event. It will log a + * message at the PEAR_LOG_ALERT log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function alert($message) + { + return $this->log($message, PEAR_LOG_ALERT); + } + + /** + * A convenience function for logging a critical event. It will log a + * message at the PEAR_LOG_CRIT log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function crit($message) + { + return $this->log($message, PEAR_LOG_CRIT); + } + + /** + * A convenience function for logging a error event. It will log a + * message at the PEAR_LOG_ERR log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function err($message) + { + return $this->log($message, PEAR_LOG_ERR); + } + + /** + * A convenience function for logging a warning event. It will log a + * message at the PEAR_LOG_WARNING log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function warning($message) + { + return $this->log($message, PEAR_LOG_WARNING); + } + + /** + * A convenience function for logging a notice event. It will log a + * message at the PEAR_LOG_NOTICE log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function notice($message) + { + return $this->log($message, PEAR_LOG_NOTICE); + } + + /** + * A convenience function for logging a information event. It will log a + * message at the PEAR_LOG_INFO log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function info($message) + { + return $this->log($message, PEAR_LOG_INFO); + } + + /** + * A convenience function for logging a debug event. It will log a + * message at the PEAR_LOG_DEBUG log level. + * + * @param mixed $message String or object containing the message + * to log. + * + * @return boolean True if the message was successfully logged. + * + * @access public + * @since Log 1.7.0 + */ + function debug($message) + { + return $this->log($message, PEAR_LOG_DEBUG); + } + + /** + * Returns the string representation of the message data. + * + * If $message is an object, _extractMessage() will attempt to extract + * the message text using a known method (such as a PEAR_Error object's + * getMessage() method). If a known method, cannot be found, the + * serialized representation of the object will be returned. + * + * If the message data is already a string, it will be returned unchanged. + * + * @param mixed $message The original message data. This may be a + * string or any object. + * + * @return string The string representation of the message. + * + * @access private + */ + function _extractMessage($message) + { + /* + * If we've been given an object, attempt to extract the message using + * a known method. If we can't find such a method, default to the + * "human-readable" version of the object. + * + * We also use the human-readable format for arrays. + */ + if (is_object($message)) { + if (method_exists($message, 'getmessage')) { + $message = $message->getMessage(); + } else if (method_exists($message, 'tostring')) { + $message = $message->toString(); + } else if (method_exists($message, '__tostring')) { + if (version_compare(PHP_VERSION, '5.0.0', 'ge')) { + $message = (string)$message; + } else { + $message = $message->__toString(); + } + } else { + $message = print_r($message, true); + } + } else if (is_array($message)) { + if (isset($message['message'])) { + $message = $message['message']; + } else { + $message = print_r($message, true); + } + } + + /* Otherwise, we assume the message is a string. */ + return $message; + } + + /** + * Using debug_backtrace(), returns the file, line, and enclosing function + * name of the source code context from which log() was invoked. + * + * @param int $depth The initial number of frames we should step + * back into the trace. + * + * @return array Array containing three strings: the filename, the line, + * and the function name from which log() was called. + * + * @access private + * @since Log 1.9.4 + */ + function _getBacktraceVars($depth) + { + /* Start by generating a backtrace from the current call (here). */ + $backtrace = debug_backtrace(); + + /* + * If we were ultimately invoked by the composite handler, we need to + * increase our depth one additional level to compensate. + */ + if (strcasecmp(@$backtrace[$depth+1]['class'], 'Log_composite') == 0) { + $depth++; + } + + /* + * We're interested in the frame which invoked the log() function, so + * we need to walk back some number of frames into the backtrace. The + * $depth parameter tells us where to start looking. We go one step + * further back to find the name of the encapsulating function from + * which log() was called. + */ + $file = @$backtrace[$depth]['file']; + $line = @$backtrace[$depth]['line']; + $func = @$backtrace[$depth + 1]['function']; + + /* + * However, if log() was called from one of our "shortcut" functions, + * we're going to need to go back an additional step. + */ + if (in_array($func, array('emerg', 'alert', 'crit', 'err', 'warning', + 'notice', 'info', 'debug'))) { + $file = @$backtrace[$depth + 1]['file']; + $line = @$backtrace[$depth + 1]['line']; + $func = @$backtrace[$depth + 2]['function']; + } + + /* + * If we couldn't extract a function name (perhaps because we were + * executed from the "main" context), provide a default value. + */ + if (is_null($func)) { + $func = '(none)'; + } + + /* Return a 3-tuple containing (file, line, function). */ + return array($file, $line, $func); + } + + /** + * Produces a formatted log line based on a format string and a set of + * variables representing the current log record and state. + * + * @return string Formatted log string. + * + * @access private + * @since Log 1.9.4 + */ + function _format($format, $timestamp, $priority, $message) + { + /* + * If the format string references any of the backtrace-driven + * variables (%5, %6, %7), generate the backtrace and fetch them. + */ + if (strpos($format, '%5') || strpos($format, '%6') || strpos($format, '%7')) { + list($file, $line, $func) = $this->_getBacktraceVars(2); + } + + /* + * Build the formatted string. We use the sprintf() function's + * "argument swapping" capability to dynamically select and position + * the variables which will ultimately appear in the log string. + */ + return sprintf($format, + $timestamp, + $this->_ident, + $this->priorityToString($priority), + $message, + isset($file) ? $file : '', + isset($line) ? $line : '', + isset($func) ? $func : ''); + } + + /** + * Returns the string representation of a PEAR_LOG_* integer constant. + * + * @param int $priority A PEAR_LOG_* integer constant. + * + * @return string The string representation of $level. + * + * @since Log 1.0 + */ + function priorityToString($priority) + { + $levels = array( + PEAR_LOG_EMERG => 'emergency', + PEAR_LOG_ALERT => 'alert', + PEAR_LOG_CRIT => 'critical', + PEAR_LOG_ERR => 'error', + PEAR_LOG_WARNING => 'warning', + PEAR_LOG_NOTICE => 'notice', + PEAR_LOG_INFO => 'info', + PEAR_LOG_DEBUG => 'debug' + ); + + return $levels[$priority]; + } + + /** + * Returns the the PEAR_LOG_* integer constant for the given string + * representation of a priority name. This function performs a + * case-insensitive search. + * + * @param string $name String containing a priority name. + * + * @return string The PEAR_LOG_* integer contstant corresponding + * the the specified priority name. + * + * @since Log 1.9.0 + */ + function stringToPriority($name) + { + $levels = array( + 'emergency' => PEAR_LOG_EMERG, + 'alert' => PEAR_LOG_ALERT, + 'critical' => PEAR_LOG_CRIT, + 'error' => PEAR_LOG_ERR, + 'warning' => PEAR_LOG_WARNING, + 'notice' => PEAR_LOG_NOTICE, + 'info' => PEAR_LOG_INFO, + 'debug' => PEAR_LOG_DEBUG + ); + + return $levels[strtolower($name)]; + } + + /** + * Calculate the log mask for the given priority. + * + * This method may be called statically. + * + * @param integer $priority The priority whose mask will be calculated. + * + * @return integer The calculated log mask. + * + * @access public + * @since Log 1.7.0 + */ + function MASK($priority) + { + return (1 << $priority); + } + + /** + * Calculate the log mask for all priorities up to the given priority. + * + * This method may be called statically. + * + * @param integer $priority The maximum priority covered by this mask. + * + * @return integer The resulting log mask. + * + * @access public + * @since Log 1.7.0 + * + * @deprecated deprecated since Log 1.9.4; use Log::MAX() instead + */ + function UPTO($priority) + { + return Log::MAX($priority); + } + + /** + * Calculate the log mask for all priorities greater than or equal to the + * given priority. In other words, $priority will be the lowest priority + * matched by the resulting mask. + * + * This method may be called statically. + * + * @param integer $priority The minimum priority covered by this mask. + * + * @return integer The resulting log mask. + * + * @access public + * @since Log 1.9.4 + */ + function MIN($priority) + { + return PEAR_LOG_ALL ^ ((1 << $priority) - 1); + } + + /** + * Calculate the log mask for all priorities less than or equal to the + * given priority. In other words, $priority will be the highests priority + * matched by the resulting mask. + * + * This method may be called statically. + * + * @param integer $priority The maximum priority covered by this mask. + * + * @return integer The resulting log mask. + * + * @access public + * @since Log 1.9.4 + */ + function MAX($priority) + { + return ((1 << ($priority + 1)) - 1); + } + + /** + * Set and return the level mask for the current Log instance. + * + * @param integer $mask A bitwise mask of log levels. + * + * @return integer The current level mask. + * + * @access public + * @since Log 1.7.0 + */ + function setMask($mask) + { + $this->_mask = $mask; + + return $this->_mask; + } + + /** + * Returns the current level mask. + * + * @return interger The current level mask. + * + * @access public + * @since Log 1.7.0 + */ + function getMask() + { + return $this->_mask; + } + + /** + * Check if the given priority is included in the current level mask. + * + * @param integer $priority The priority to check. + * + * @return boolean True if the given priority is included in the current + * log mask. + * + * @access private + * @since Log 1.7.0 + */ + function _isMasked($priority) + { + return (Log::MASK($priority) & $this->_mask); + } + + /** + * Returns the current default priority. + * + * @return integer The current default priority. + * + * @access public + * @since Log 1.8.4 + */ + function getPriority() + { + return $this->_priority; + } + + /** + * Sets the default priority to the specified value. + * + * @param integer $priority The new default priority. + * + * @access public + * @since Log 1.8.4 + */ + function setPriority($priority) + { + $this->_priority = $priority; + } + + /** + * Adds a Log_observer instance to the list of observers that are listening + * for messages emitted by this Log instance. + * + * @param object $observer The Log_observer instance to attach as a + * listener. + * + * @param boolean True if the observer is successfully attached. + * + * @access public + * @since Log 1.0 + */ + function attach(&$observer) + { + if (!is_a($observer, 'Log_observer')) { + return false; + } + + $this->_listeners[$observer->_id] = &$observer; + + return true; + } + + /** + * Removes a Log_observer instance from the list of observers. + * + * @param object $observer The Log_observer instance to detach from + * the list of listeners. + * + * @param boolean True if the observer is successfully detached. + * + * @access public + * @since Log 1.0 + */ + function detach($observer) + { + if (!is_a($observer, 'Log_observer') || + !isset($this->_listeners[$observer->_id])) { + return false; + } + + unset($this->_listeners[$observer->_id]); + + return true; + } + + /** + * Informs each registered observer instance that a new message has been + * logged. + * + * @param array $event A hash describing the log event. + * + * @access private + */ + function _announce($event) + { + foreach ($this->_listeners as $id => $listener) { + if ($event['priority'] <= $this->_listeners[$id]->_priority) { + $this->_listeners[$id]->notify($event); + } + } + } + + /** + * Indicates whether this is a composite class. + * + * @return boolean True if this is a composite class. + * + * @access public + * @since Log 1.0 + */ + function isComposite() + { + return false; + } + + /** + * Sets this Log instance's identification string. + * + * @param string $ident The new identification string. + * + * @access public + * @since Log 1.6.3 + */ + function setIdent($ident) + { + $this->_ident = $ident; + } + + /** + * Returns the current identification string. + * + * @return string The current Log instance's identification string. + * + * @access public + * @since Log 1.6.3 + */ + function getIdent() + { + return $this->_ident; + } +} diff --git a/Log/composite.php b/Log/composite.php new file mode 100644 index 0000000..98a1d81 --- /dev/null +++ b/Log/composite.php @@ -0,0 +1,231 @@ + + * @author Jon Parise + * + * @since Horde 1.3 + * @since Log 1.0 + * @package Log + * + * @example composite.php Using the composite handler. + */ +class Log_composite extends Log +{ + /** + * Array holding all of the Log instances to which log events should be + * sent. + * + * @var array + * @access private + */ + var $_children = array(); + + + /** + * Constructs a new composite Log object. + * + * @param boolean $name This parameter is ignored. + * @param boolean $ident This parameter is ignored. + * @param boolean $conf This parameter is ignored. + * @param boolean $level This parameter is ignored. + * + * @access public + */ + function Log_composite($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_ident = $ident; + } + + /** + * Opens all of the child instances. + * + * @return True if all of the child instances were successfully opened. + * + * @access public + */ + function open() + { + /* Attempt to open each of our children. */ + $this->_opened = true; + foreach ($this->_children as $id => $child) { + $this->_opened &= $this->_children[$id]->open(); + } + + /* If all children were opened, return success. */ + return $this->_opened; + } + + /** + * Closes all of the child instances. + * + * @return True if all of the child instances were successfully closed. + * + * @access public + */ + function close() + { + /* Attempt to close each of our children. */ + $closed = true; + foreach ($this->_children as $id => $child) { + $closed &= $this->_children[$id]->close(); + } + + /* Track the _opened state for consistency. */ + $this->_opened = false; + + /* If all children were closed, return success. */ + return $closed; + } + + /** + * Flushes all child instances. It is assumed that all of the children + * have been successfully opened. + * + * @return True if all of the child instances were successfully flushed. + * + * @access public + * @since Log 1.8.2 + */ + function flush() + { + /* Attempt to flush each of our children. */ + $flushed = true; + foreach ($this->_children as $id => $child) { + $flushed &= $this->_children[$id]->flush(); + } + + /* If all children were flushed, return success. */ + return $flushed; + } + + /** + * Sends $message and $priority to each child of this composite. If the + * children aren't already open, they will be opened here. + * + * @param mixed $message String or object containing the message + * to log. + * @param string $priority (optional) The priority of the message. + * Valid values are: PEAR_LOG_EMERG, + * PEAR_LOG_ALERT, PEAR_LOG_CRIT, + * PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and + * PEAR_LOG_DEBUG. + * + * @return boolean True if the entry is successfully logged. + * + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* + * If the handlers haven't been opened, attempt to open them now. + * However, we don't treat failure to open all of the handlers as a + * fatal error. We defer that consideration to the success of calling + * each handler's log() method below. + */ + if (!$this->_opened) { + $this->open(); + } + + /* Attempt to log the event using each of the children. */ + $success = true; + foreach ($this->_children as $id => $child) { + $success &= $this->_children[$id]->log($message, $priority); + } + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + /* Return success if all of the children logged the event. */ + return $success; + } + + /** + * Returns true if this is a composite. + * + * @return boolean True if this is a composite class. + * + * @access public + */ + function isComposite() + { + return true; + } + + /** + * Sets this identification string for all of this composite's children. + * + * @param string $ident The new identification string. + * + * @access public + * @since Log 1.6.7 + */ + function setIdent($ident) + { + /* Call our base class's setIdent() method. */ + parent::setIdent($ident); + + /* ... and then call setIdent() on all of our children. */ + foreach ($this->_children as $id => $child) { + $this->_children[$id]->setIdent($ident); + } + } + + /** + * Adds a Log instance to the list of children. + * + * @param object $child The Log instance to add. + * + * @return boolean True if the Log instance was successfully added. + * + * @access public + */ + function addChild(&$child) + { + /* Make sure this is a Log instance. */ + if (!is_a($child, 'Log')) { + return false; + } + + $this->_children[$child->_id] = &$child; + + return true; + } + + /** + * Removes a Log instance from the list of children. + * + * @param object $child The Log instance to remove. + * + * @return boolean True if the Log instance was successfully removed. + * + * @access public + */ + function removeChild($child) + { + if (!is_a($child, 'Log') || !isset($this->_children[$child->_id])) { + return false; + } + + unset($this->_children[$child->_id]); + + return true; + } + +} diff --git a/Log/console.php b/Log/console.php new file mode 100644 index 0000000..0c1f9b6 --- /dev/null +++ b/Log/console.php @@ -0,0 +1,208 @@ + + * @since Log 1.1 + * @package Log + * + * @example console.php Using the console handler. + */ +class Log_console extends Log +{ + /** + * Handle to the current output stream. + * @var resource + * @access private + */ + var $_stream = STDOUT; + + /** + * Should the output be buffered or displayed immediately? + * @var string + * @access private + */ + var $_buffering = false; + + /** + * String holding the buffered output. + * @var string + * @access private + */ + var $_buffer = ''; + + /** + * String containing the format of a log line. + * @var string + * @access private + */ + var $_lineFormat = '%1$s %2$s [%3$s] %4$s'; + + /** + * String containing the timestamp format. It will be passed directly to + * strftime(). Note that the timestamp string will generated using the + * current locale. + * @var string + * @access private + */ + var $_timeFormat = '%b %d %H:%M:%S'; + + /** + * Constructs a new Log_console object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_console($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (!empty($conf['stream'])) { + $this->_stream = $conf['stream']; + } + + if (isset($conf['buffering'])) { + $this->_buffering = $conf['buffering']; + } + + if (!empty($conf['lineFormat'])) { + $this->_lineFormat = str_replace(array_keys($this->_formatMap), + array_values($this->_formatMap), + $conf['lineFormat']); + } + + if (!empty($conf['timeFormat'])) { + $this->_timeFormat = $conf['timeFormat']; + } + + /* + * If output buffering has been requested, we need to register a + * shutdown function that will dump the buffer upon termination. + */ + if ($this->_buffering) { + register_shutdown_function(array(&$this, '_Log_console')); + } + } + + /** + * Destructor + */ + function _Log_console() + { + $this->close(); + } + + /** + * Open the output stream. + * + * @access public + * @since Log 1.9.7 + */ + function open() + { + $this->_opened = true; + return true; + } + + /** + * Closes the output stream. + * + * This results in a call to flush(). + * + * @access public + * @since Log 1.9.0 + */ + function close() + { + $this->flush(); + $this->_opened = false; + return true; + } + + /** + * Flushes all pending ("buffered") data to the output stream. + * + * @access public + * @since Log 1.8.2 + */ + function flush() + { + /* + * If output buffering is enabled, dump the contents of the buffer to + * the output stream. + */ + if ($this->_buffering && (strlen($this->_buffer) > 0)) { + fwrite($this->_stream, $this->_buffer); + $this->_buffer = ''; + } + + if (is_resource($this->_stream)) { + return fflush($this->_stream); + } + + return false; + } + + /** + * Writes $message to the text console. Also, passes the message + * along to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build the string containing the complete log line. */ + $line = $this->_format($this->_lineFormat, + strftime($this->_timeFormat), + $priority, $message) . "\n"; + + /* + * If buffering is enabled, append this line to the output buffer. + * Otherwise, print the line to the output stream immediately. + */ + if ($this->_buffering) { + $this->_buffer .= $line; + } else { + fwrite($this->_stream, $line); + } + + /* Notify observers about this log message. */ + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/Log/daemon.php b/Log/daemon.php new file mode 100644 index 0000000..9873f2a --- /dev/null +++ b/Log/daemon.php @@ -0,0 +1,230 @@ + + * @version $Revision: 1.2 $ + * @package Log + */ +class Log_daemon extends Log +{ + /** + * Integer holding the log facility to use. + * @var string + */ + var $_name = LOG_DAEMON; + + /** + * Var holding the resource pointer to the socket + * @var resource + */ + var $_socket; + + /** + * The ip address or servername + * @see http://www.php.net/manual/en/transports.php + * @var string + */ + var $_ip = '127.0.0.1'; + + /** + * Protocol to use (tcp, udp, etc.) + * @see http://www.php.net/manual/en/transports.php + * @var string + */ + var $_proto = 'udp'; + + /** + * Port to connect to + * @var int + */ + var $_port = 514; + + /** + * Maximum message length in bytes + * @var int + */ + var $_maxsize = 4096; + + /** + * Socket timeout in seconds + * @var int + */ + var $_timeout = 1; + + + /** + * Constructs a new syslog object. + * + * @param string $name The syslog facility. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $maxLevel Maximum level at which to log. + * @access public + */ + function Log_daemon($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + /* Ensure we have a valid integer value for $name. */ + if (empty($name) || !is_int($name)) { + $name = LOG_SYSLOG; + } + + $this->_id = md5(microtime()); + $this->_name = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (isset($conf['ip'])) { + $this->_ip = $conf['ip']; + } + if (isset($conf['proto'])) { + $this->_proto = $conf['proto']; + } + if (isset($conf['port'])) { + $this->_port = $conf['port']; + } + if (isset($conf['maxsize'])) { + $this->_maxsize = $conf['maxsize']; + } + if (isset($conf['timeout'])) { + $this->_timeout = $conf['timeout']; + } + $this->_proto = $this->_proto . '://'; + + register_shutdown_function(array(&$this, '_Log_daemon')); + } + + /** + * Destructor. + * + * @access private + */ + function _Log_daemon() + { + $this->close(); + } + + /** + * Opens a connection to the system logger, if it has not already + * been opened. This is implicitly called by log(), if necessary. + * @access public + */ + function open() + { + if (!$this->_opened) { + $this->_opened = (bool)($this->_socket = @fsockopen( + $this->_proto . $this->_ip, + $this->_port, + $errno, + $errstr, + $this->_timeout)); + } + return $this->_opened; + } + + /** + * Closes the connection to the system logger, if it is open. + * @access public + */ + function close() + { + if ($this->_opened) { + $this->_opened = false; + return fclose($this->_socket); + } + return true; + } + + /** + * Sends $message to the currently open syslog connection. Calls + * open() if necessary. Also passes the message along to any Log_observer + * instances that are observing this Log. + * + * @param string $message The textual message to be logged. + * @param int $priority (optional) The priority of the message. Valid + * values are: LOG_EMERG, LOG_ALERT, LOG_CRIT, + * LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, + * and LOG_DEBUG. The default is LOG_INFO. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Set the facility level. */ + $facility_level = intval($this->_name) + + intval($this->_toSyslog($priority)); + + /* Prepend ident info. */ + if (!empty($this->_ident)) { + $message = $this->_ident . ' ' . $message; + } + + /* Check for message length. */ + if (strlen($message) > $this->_maxsize) { + $message = substr($message, 0, ($this->_maxsize) - 10) . ' [...]'; + } + + /* Write to socket. */ + fwrite($this->_socket, '<' . $facility_level . '>' . $message . "\n"); + + $this->_announce(array('priority' => $priority, 'message' => $message)); + } + + /** + * Converts a PEAR_LOG_* constant into a syslog LOG_* constant. + * + * This function exists because, under Windows, not all of the LOG_* + * constants have unique values. Instead, the PEAR_LOG_* were introduced + * for global use, with the conversion to the LOG_* constants kept local to + * to the syslog driver. + * + * @param int $priority PEAR_LOG_* value to convert to LOG_* value. + * + * @return The LOG_* representation of $priority. + * + * @access private + */ + function _toSyslog($priority) + { + static $priorities = array( + PEAR_LOG_EMERG => LOG_EMERG, + PEAR_LOG_ALERT => LOG_ALERT, + PEAR_LOG_CRIT => LOG_CRIT, + PEAR_LOG_ERR => LOG_ERR, + PEAR_LOG_WARNING => LOG_WARNING, + PEAR_LOG_NOTICE => LOG_NOTICE, + PEAR_LOG_INFO => LOG_INFO, + PEAR_LOG_DEBUG => LOG_DEBUG + ); + + /* If we're passed an unknown priority, default to LOG_INFO. */ + if (!is_int($priority) || !in_array($priority, $priorities)) { + return LOG_INFO; + } + + return $priorities[$priority]; + } + +} diff --git a/Log/display.php b/Log/display.php new file mode 100644 index 0000000..31ad1e7 --- /dev/null +++ b/Log/display.php @@ -0,0 +1,141 @@ + + * @since Log 1.8.0 + * @package Log + * + * @example display.php Using the display handler. + */ +class Log_display extends Log +{ + /** + * String to output before an error message + * @var string + * @access private + */ + var $_error_prepend = ''; + + /** + * String to output after an error message + * @var string + * @access private + */ + var $_error_append = ''; + + /** + * String used to represent a line break. + * @var string + * @access private + */ + var $_linebreak = "
\n"; + + /** + * Constructs a new Log_display object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_display($name = '', $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (isset($conf['error_prepend'])) { + $this->_error_prepend = $conf['error_prepend']; + } else { + $this->_error_prepend = ini_get('error_prepend_string'); + } + + if (isset($conf['error_append'])) { + $this->_error_append = $conf['error_append']; + } else { + $this->_error_append = ini_get('error_append_string'); + } + + if (isset($conf['linebreak'])) { + $this->_linebreak = $conf['linebreak']; + } + } + + /** + * Opens the display handler. + * + * @access public + * @since Log 1.9.6 + */ + function open() + { + $this->_opened = true; + return true; + } + + /** + * Closes the display handler. + * + * @access public + * @since Log 1.9.6 + */ + function close() + { + $this->_opened = false; + return true; + } + + /** + * Writes $message to the text browser. Also, passes the message + * along to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build and output the complete log line. */ + echo $this->_error_prepend . + '' . ucfirst($this->priorityToString($priority)) . ': '. + nl2br(htmlspecialchars($message)) . + $this->_error_append . $this->_linebreak; + + /* Notify observers about this log message. */ + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/Log/error_log.php b/Log/error_log.php new file mode 100644 index 0000000..117e96c --- /dev/null +++ b/Log/error_log.php @@ -0,0 +1,127 @@ + + * @since Log 1.7.0 + * @package Log + * + * @example error_log.php Using the error_log handler. + */ +class Log_error_log extends Log +{ + /** + * The error_log() log type. + * @var integer + * @access private + */ + var $_type = PEAR_LOG_TYPE_SYSTEM; + + /** + * The type-specific destination value. + * @var string + * @access private + */ + var $_destination = ''; + + /** + * Additional headers to pass to the mail() function when the + * PEAR_LOG_TYPE_MAIL type is used. + * @var string + * @access private + */ + var $_extra_headers = ''; + + /** + * Constructs a new Log_error_log object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_error_log($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_type = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (!empty($conf['destination'])) { + $this->_destination = $conf['destination']; + } + if (!empty($conf['extra_headers'])) { + $this->_extra_headers = $conf['extra_headers']; + } + } + + /** + * Opens the handler. + * + * @access public + * @since Log 1.9.6 + */ + function open() + { + $this->_opened = true; + return true; + } + + /** + * Closes the handler. + * + * @access public + * @since Log 1.9.6 + */ + function close() + { + $this->_opened = false; + return true; + } + + /** + * Logs $message using PHP's error_log() function. The message is also + * passed along to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + $success = error_log($this->_ident . ': ' . $message, $this->_type, + $this->_destination, $this->_extra_headers); + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return $success; + } + +} diff --git a/Log/file.php b/Log/file.php new file mode 100644 index 0000000..943eeca --- /dev/null +++ b/Log/file.php @@ -0,0 +1,316 @@ + + * @author Roman Neuhauser + * @since Log 1.0 + * @package Log + * + * @example file.php Using the file handler. + */ +class Log_file extends Log +{ + /** + * String containing the name of the log file. + * @var string + * @access private + */ + var $_filename = 'php.log'; + + /** + * Handle to the log file. + * @var resource + * @access private + */ + var $_fp = false; + + /** + * Should new log entries be append to an existing log file, or should the + * a new log file overwrite an existing one? + * @var boolean + * @access private + */ + var $_append = true; + + /** + * Should advisory file locking (i.e., flock()) be used? + * @var boolean + * @access private + */ + var $_locking = false; + + /** + * Integer (in octal) containing the log file's permissions mode. + * @var integer + * @access private + */ + var $_mode = 0644; + + /** + * Integer (in octal) specifying the file permission mode that will be + * used when creating directories that do not already exist. + * @var integer + * @access private + */ + var $_dirmode = 0755; + + /** + * String containing the format of a log line. + * @var string + * @access private + */ + var $_lineFormat = '%1$s %2$s [%3$s] %4$s'; + + /** + * String containing the timestamp format. It will be passed directly to + * strftime(). Note that the timestamp string will generated using the + * current locale. + * @var string + * @access private + */ + var $_timeFormat = '%b %d %H:%M:%S'; + + /** + * String containing the end-on-line character sequence. + * @var string + * @access private + */ + var $_eol = "\n"; + + /** + * Constructs a new Log_file object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_file($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_filename = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (isset($conf['append'])) { + $this->_append = $conf['append']; + } + + if (isset($conf['locking'])) { + $this->_locking = $conf['locking']; + } + + if (!empty($conf['mode'])) { + if (is_string($conf['mode'])) { + $this->_mode = octdec($conf['mode']); + } else { + $this->_mode = $conf['mode']; + } + } + + if (!empty($conf['dirmode'])) { + if (is_string($conf['dirmode'])) { + $this->_dirmode = octdec($conf['dirmode']); + } else { + $this->_dirmode = $conf['dirmode']; + } + } + + if (!empty($conf['lineFormat'])) { + $this->_lineFormat = str_replace(array_keys($this->_formatMap), + array_values($this->_formatMap), + $conf['lineFormat']); + } + + if (!empty($conf['timeFormat'])) { + $this->_timeFormat = $conf['timeFormat']; + } + + if (!empty($conf['eol'])) { + $this->_eol = $conf['eol']; + } else { + $this->_eol = (strstr(PHP_OS, 'WIN')) ? "\r\n" : "\n"; + } + + register_shutdown_function(array(&$this, '_Log_file')); + } + + /** + * Destructor + */ + function _Log_file() + { + if ($this->_opened) { + $this->close(); + } + } + + /** + * Creates the given directory path. If the parent directories don't + * already exist, they will be created, too. + * + * This implementation is inspired by Python's os.makedirs function. + * + * @param string $path The full directory path to create. + * @param integer $mode The permissions mode with which the + * directories will be created. + * + * @return True if the full path is successfully created or already + * exists. + * + * @access private + */ + function _mkpath($path, $mode = 0700) + { + /* Separate the last pathname component from the rest of the path. */ + $head = dirname($path); + $tail = basename($path); + + /* Make sure we've split the path into two complete components. */ + if (empty($tail)) { + $head = dirname($path); + $tail = basename($path); + } + + /* Recurse up the path if our current segment does not exist. */ + if (!empty($head) && !empty($tail) && !is_dir($head)) { + $this->_mkpath($head, $mode); + } + + /* Create this segment of the path. */ + return @mkdir($head, $mode); + } + + /** + * Opens the log file for output. If the specified log file does not + * already exist, it will be created. By default, new log entries are + * appended to the end of the log file. + * + * This is implicitly called by log(), if necessary. + * + * @access public + */ + function open() + { + if (!$this->_opened) { + /* If the log file's directory doesn't exist, create it. */ + if (!is_dir(dirname($this->_filename))) { + $this->_mkpath($this->_filename, $this->_dirmode); + } + + /* Determine whether the log file needs to be created. */ + $creating = !file_exists($this->_filename); + + /* Obtain a handle to the log file. */ + $this->_fp = fopen($this->_filename, ($this->_append) ? 'a' : 'w'); + + /* We consider the file "opened" if we have a valid file pointer. */ + $this->_opened = ($this->_fp !== false); + + /* Attempt to set the file's permissions if we just created it. */ + if ($creating && $this->_opened) { + chmod($this->_filename, $this->_mode); + } + } + + return $this->_opened; + } + + /** + * Closes the log file if it is open. + * + * @access public + */ + function close() + { + /* If the log file is open, close it. */ + if ($this->_opened && fclose($this->_fp)) { + $this->_opened = false; + } + + return ($this->_opened === false); + } + + /** + * Flushes all pending data to the file handle. + * + * @access public + * @since Log 1.8.2 + */ + function flush() + { + if (is_resource($this->_fp)) { + return fflush($this->_fp); + } + + return false; + } + + /** + * Logs $message to the output window. The message is also passed along + * to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the log file isn't already open, open it now. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build the string containing the complete log line. */ + $line = $this->_format($this->_lineFormat, + strftime($this->_timeFormat), + $priority, $message) . $this->_eol; + + /* If locking is enabled, acquire an exclusive lock on the file. */ + if ($this->_locking) { + flock($this->_fp, LOCK_EX); + } + + /* Write the log line to the log file. */ + $success = (fwrite($this->_fp, $line) !== false); + + /* Unlock the file now that we're finished writing to it. */ + if ($this->_locking) { + flock($this->_fp, LOCK_UN); + } + + /* Notify observers about this log message. */ + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return $success; + } + +} diff --git a/Log/firebug.php b/Log/firebug.php new file mode 100644 index 0000000..bfea5d3 --- /dev/null +++ b/Log/firebug.php @@ -0,0 +1,210 @@ + + * @since Log 1.x.x + * @package Log + * + * @example firebug.php Using the firebug handler. + */ +class Log_firebug extends Log +{ + /** + * Should the output be buffered or displayed immediately? + * @var string + * @access private + */ + var $_buffering = false; + + /** + * String holding the buffered output. + * @var string + * @access private + */ + var $_buffer = array(); + + /** + * String containing the format of a log line. + * @var string + * @access private + */ + var $_lineFormat = '%2$s [%3$s] %4$s'; + + /** + * String containing the timestamp format. It will be passed directly to + * strftime(). Note that the timestamp string will generated using the + * current locale. + * + * Note! Default lineFormat of this driver does not display time. + * + * @var string + * @access private + */ + var $_timeFormat = '%b %d %H:%M:%S'; + + /** + * Mapping of log priorities to Firebug methods. + * @var array + * @access private + */ + var $_methods = array( + PEAR_LOG_EMERG => 'error', + PEAR_LOG_ALERT => 'error', + PEAR_LOG_CRIT => 'error', + PEAR_LOG_ERR => 'error', + PEAR_LOG_WARNING => 'warn', + PEAR_LOG_NOTICE => 'info', + PEAR_LOG_INFO => 'info', + PEAR_LOG_DEBUG => 'debug' + ); + + /** + * Constructs a new Log_firebug object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_firebug($name = '', $ident = 'PHP', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + if (isset($conf['buffering'])) { + $this->_buffering = $conf['buffering']; + } + + if ($this->_buffering) { + register_shutdown_function(array(&$this, '_Log_firebug')); + } + + if (!empty($conf['lineFormat'])) { + $this->_lineFormat = str_replace(array_keys($this->_formatMap), + array_values($this->_formatMap), + $conf['lineFormat']); + } + + if (!empty($conf['timeFormat'])) { + $this->_timeFormat = $conf['timeFormat']; + } + } + + /** + * Opens the firebug handler. + * + * @access public + */ + function open() + { + $this->_opened = true; + return true; + } + + /** + * Destructor + */ + function _Log_firebug() + { + $this->close(); + } + + /** + * Closes the firebug handler. + * + * @access public + */ + function close() + { + $this->flush(); + $this->_opened = false; + return true; + } + + /** + * Flushes all pending ("buffered") data. + * + * @access public + */ + function flush() { + if (count($this->_buffer)) { + print '\n"; + }; + $this->_buffer = array(); + } + + /** + * Writes $message to Firebug console. Also, passes the message + * along to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + $method = $this->_methods[$priority]; + + /* normalize line breaks */ + $message = str_replace("\r\n", "\n", $message); + + /* escape line breaks */ + $message = str_replace("\n", "\\n\\\n", $message); + + /* escape quotes */ + $message = str_replace('"', '\\"', $message); + + /* Build the string containing the complete log line. */ + $line = $this->_format($this->_lineFormat, + strftime($this->_timeFormat), + $priority, + $message); + + if ($this->_buffering) { + $this->_buffer[] = sprintf('console.%s("%s");', $method, $line); + } else { + print '\n"; + } + /* Notify observers about this log message. */ + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/Log/mail.php b/Log/mail.php new file mode 100644 index 0000000..d4eea6c --- /dev/null +++ b/Log/mail.php @@ -0,0 +1,257 @@ + + * @author Jon Parise + * @since Log 1.3 + * @package Log + * + * @example mail.php Using the mail handler. + */ +class Log_mail extends Log +{ + /** + * String holding the recipients' email addresses. Multiple addresses + * should be separated with commas. + * @var string + * @access private + */ + var $_recipients = ''; + + /** + * String holding the sender's email address. + * @var string + * @access private + */ + var $_from = ''; + + /** + * String holding the email's subject. + * @var string + * @access private + */ + var $_subject = '[Log_mail] Log message'; + + /** + * String holding an optional preamble for the log messages. + * @var string + * @access private + */ + var $_preamble = ''; + + /** + * String containing the format of a log line. + * @var string + * @access private + */ + var $_lineFormat = '%1$s %2$s [%3$s] %4$s'; + + /** + * String containing the timestamp format. It will be passed directly to + * strftime(). Note that the timestamp string will generated using the + * current locale. + * @var string + * @access private + */ + var $_timeFormat = '%b %d %H:%M:%S'; + + /** + * String holding the mail message body. + * @var string + * @access private + */ + var $_message = ''; + + /** + * Flag used to indicated that log lines have been written to the message + * body and the message should be sent on close(). + * @var boolean + * @access private + */ + var $_shouldSend = false; + + /** + * Constructs a new Log_mail object. + * + * Here is how you can customize the mail driver with the conf[] hash : + * $conf['from'] : the mail's "From" header line, + * $conf['subject'] : the mail's "Subject" line. + * + * @param string $name The message's recipients. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_mail($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_recipients = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (!empty($conf['from'])) { + $this->_from = $conf['from']; + } else { + $this->_from = ini_get('sendmail_from'); + } + + if (!empty($conf['subject'])) { + $this->_subject = $conf['subject']; + } + + if (!empty($conf['preamble'])) { + $this->_preamble = $conf['preamble']; + } + + if (!empty($conf['lineFormat'])) { + $this->_lineFormat = str_replace(array_keys($this->_formatMap), + array_values($this->_formatMap), + $conf['lineFormat']); + } + + if (!empty($conf['timeFormat'])) { + $this->_timeFormat = $conf['timeFormat']; + } + + /* register the destructor */ + register_shutdown_function(array(&$this, '_Log_mail')); + } + + /** + * Destructor. Calls close(). + * + * @access private + */ + function _Log_mail() + { + $this->close(); + } + + /** + * Starts a new mail message. + * This is implicitly called by log(), if necessary. + * + * @access public + */ + function open() + { + if (!$this->_opened) { + if (!empty($this->_preamble)) { + $this->_message = $this->_preamble . "\r\n\r\n"; + } + $this->_opened = true; + $_shouldSend = false; + } + + return $this->_opened; + } + + /** + * Closes the message, if it is open, and sends the mail. + * This is implicitly called by the destructor, if necessary. + * + * @access public + */ + function close() + { + if ($this->_opened) { + if ($this->_shouldSend && !empty($this->_message)) { + $headers = "From: $this->_from\r\n"; + $headers .= "User-Agent: Log_mail"; + + if (mail($this->_recipients, $this->_subject, $this->_message, + $headers) == false) { + error_log("Log_mail: Failure executing mail()", 0); + return false; + } + + /* Clear the message string now that the email has been sent. */ + $this->_message = ''; + $this->_shouldSend = false; + } + $this->_opened = false; + } + + return ($this->_opened === false); + } + + /** + * Flushes the log output by forcing the email message to be sent now. + * Events that are logged after flush() is called will be appended to a + * new email message. + * + * @access public + * @since Log 1.8.2 + */ + function flush() + { + /* + * It's sufficient to simply call close() to flush the output. + * The next call to log() will cause the handler to be reopened. + */ + return $this->close(); + } + + /** + * Writes $message to the currently open mail message. + * Calls open(), if necessary. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the message isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Append the string containing the complete log line. */ + $this->_message .= $this->_format($this->_lineFormat, + strftime($this->_timeFormat), + $priority, $message) . "\r\n"; + $this->_shouldSend = true; + + /* Notify observers about this log message. */ + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } +} diff --git a/Log/mcal.php b/Log/mcal.php new file mode 100644 index 0000000..ab88d45 --- /dev/null +++ b/Log/mcal.php @@ -0,0 +1,170 @@ + + * @since Horde 1.3 + * @since Log 1.0 + * @package Log + */ +class Log_mcal extends Log +{ + /** + * holding the calendar specification to connect to. + * @var string + * @access private + */ + var $_calendar = '{localhost/mstore}'; + + /** + * holding the username to use. + * @var string + * @access private + */ + var $_username = ''; + + /** + * holding the password to use. + * @var string + * @access private + */ + var $_password = ''; + + /** + * holding the options to pass to the calendar stream. + * @var integer + * @access private + */ + var $_options = 0; + + /** + * ResourceID of the MCAL stream. + * @var string + * @access private + */ + var $_stream = ''; + + /** + * Integer holding the log facility to use. + * @var string + * @access private + */ + var $_name = LOG_SYSLOG; + + + /** + * Constructs a new Log_mcal object. + * + * @param string $name The category to use for our events. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_mcal($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_name = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + $this->_calendar = $conf['calendar']; + $this->_username = $conf['username']; + $this->_password = $conf['password']; + $this->_options = $conf['options']; + } + + /** + * Opens a calendar stream, if it has not already been + * opened. This is implicitly called by log(), if necessary. + * @access public + */ + function open() + { + if (!$this->_opened) { + $this->_stream = mcal_open($this->_calendar, $this->_username, + $this->_password, $this->_options); + $this->_opened = true; + } + + return $this->_opened; + } + + /** + * Closes the calendar stream, if it is open. + * @access public + */ + function close() + { + if ($this->_opened) { + mcal_close($this->_stream); + $this->_opened = false; + } + + return ($this->_opened === false); + } + + /** + * Logs $message and associated information to the currently open + * calendar stream. Calls open() if necessary. Also passes the + * message along to any Log_observer instances that are observing + * this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + $date_str = date('Y:n:j:G:i:s'); + $dates = explode(':', $date_str); + + mcal_event_init($this->_stream); + mcal_event_set_title($this->_stream, $this->_ident); + mcal_event_set_category($this->_stream, $this->_name); + mcal_event_set_description($this->_stream, $message); + mcal_event_add_attribute($this->_stream, 'priority', $priority); + mcal_event_set_start($this->_stream, $dates[0], $dates[1], $dates[2], + $dates[3], $dates[4], $dates[5]); + mcal_event_set_end($this->_stream, $dates[0], $dates[1], $dates[2], + $dates[3], $dates[4], $dates[5]); + mcal_append_event($this->_stream); + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/Log/mdb2.php b/Log/mdb2.php new file mode 100644 index 0000000..cc06787 --- /dev/null +++ b/Log/mdb2.php @@ -0,0 +1,358 @@ + + * @author Jon Parise + * @since Log 1.9.0 + * @package Log + */ +class Log_mdb2 extends Log +{ + /** + * Variable containing the DSN information. + * @var mixed + * @access private + */ + var $_dsn = ''; + + /** + * Array containing our set of DB configuration options. + * @var array + * @access private + */ + var $_options = array('persistent' => true); + + /** + * Object holding the database handle. + * @var object + * @access private + */ + var $_db = null; + + /** + * Resource holding the prepared statement handle. + * @var resource + * @access private + */ + var $_statement = null; + + /** + * Flag indicating that we're using an existing database connection. + * @var boolean + * @access private + */ + var $_existingConnection = false; + + /** + * String holding the database table to use. + * @var string + * @access private + */ + var $_table = 'log_table'; + + /** + * String holding the name of the ID sequence. + * @var string + * @access private + */ + var $_sequence = 'log_id'; + + /** + * Maximum length of the $ident string. This corresponds to the size of + * the 'ident' column in the SQL table. + * @var integer + * @access private + */ + var $_identLimit = 16; + + /** + * Set of field types used in the database table. + * @var array + * @access private + */ + var $_types = array( + 'id' => 'integer', + 'logtime' => 'timestamp', + 'ident' => 'text', + 'priority' => 'text', + 'message' => 'clob' + ); + + /** + * Constructs a new sql logging object. + * + * @param string $name The target SQL table. + * @param string $ident The identification field. + * @param array $conf The connection configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_mdb2($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_table = $name; + $this->_mask = Log::UPTO($level); + + /* If an options array was provided, use it. */ + if (isset($conf['options']) && is_array($conf['options'])) { + $this->_options = $conf['options']; + } + + /* If a specific sequence name was provided, use it. */ + if (!empty($conf['sequence'])) { + $this->_sequence = $conf['sequence']; + } + + /* If a specific sequence name was provided, use it. */ + if (isset($conf['identLimit'])) { + $this->_identLimit = $conf['identLimit']; + } + + /* Now that the ident limit is confirmed, set the ident string. */ + $this->setIdent($ident); + + /* If an existing database connection was provided, use it. */ + if (isset($conf['db'])) { + $this->_db = &$conf['db']; + $this->_existingConnection = true; + $this->_opened = true; + } elseif (isset($conf['singleton'])) { + $this->_db = &MDB2::singleton($conf['singleton'], $this->_options); + $this->_existingConnection = true; + $this->_opened = true; + } else { + $this->_dsn = $conf['dsn']; + } + } + + /** + * Opens a connection to the database, if it has not already + * been opened. This is implicitly called by log(), if necessary. + * + * @return boolean True on success, false on failure. + * @access public + */ + function open() + { + if (!$this->_opened) { + /* Use the DSN and options to create a database connection. */ + $this->_db = &MDB2::connect($this->_dsn, $this->_options); + if (PEAR::isError($this->_db)) { + return false; + } + + /* Create a prepared statement for repeated use in log(). */ + if (!$this->_prepareStatement()) { + return false; + } + + /* We now consider out connection open. */ + $this->_opened = true; + } + + return $this->_opened; + } + + /** + * Closes the connection to the database if it is still open and we were + * the ones that opened it. It is the caller's responsible to close an + * existing connection that was passed to us via $conf['db']. + * + * @return boolean True on success, false on failure. + * @access public + */ + function close() + { + /* If we have a statement object, free it. */ + if (is_object($this->_statement)) { + $this->_statement->free(); + $this->_statement = null; + } + + /* If we opened the database connection, disconnect it. */ + if ($this->_opened && !$this->_existingConnection) { + $this->_opened = false; + return $this->_db->disconnect(); + } + + return ($this->_opened === false); + } + + /** + * Sets this Log instance's identification string. Note that this + * SQL-specific implementation will limit the length of the $ident string + * to sixteen (16) characters. + * + * @param string $ident The new identification string. + * + * @access public + * @since Log 1.8.5 + */ + function setIdent($ident) + { + $this->_ident = substr($ident, 0, $this->_identLimit); + } + + /** + * Inserts $message to the currently open database. Calls open(), + * if necessary. Also passes the message along to any Log_observer + * instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* If we don't already have a statement object, create one. */ + if (!is_object($this->_statement) && !$this->_prepareStatement()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build our set of values for this log entry. */ + $values = array( + 'id' => $this->_db->nextId($this->_sequence), + 'logtime' => MDB2_Date::mdbNow(), + 'ident' => $this->_ident, + 'priority' => $priority, + 'message' => $message + ); + + /* Execute the SQL query for this log entry insertion. */ + $this->_db->expectError(MDB2_ERROR_NOSUCHTABLE); + $result = &$this->_statement->execute($values); + $this->_db->popExpect(); + + /* Attempt to handle any errors. */ + if (PEAR::isError($result)) { + /* We can only handle MDB2_ERROR_NOSUCHTABLE errors. */ + if ($result->getCode() != MDB2_ERROR_NOSUCHTABLE) { + return false; + } + + /* Attempt to create the target table. */ + if (!$this->_createTable()) { + return false; + } + + /* Recreate our prepared statement resource. */ + $this->_statement->free(); + if (!$this->_prepareStatement()) { + return false; + } + + /* Attempt to re-execute the insertion query. */ + $result = $this->_statement->execute($values); + if (PEAR::isError($result)) { + return false; + } + } + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + + /** + * Create the log table in the database. + * + * @return boolean True on success or false on failure. + * @access private + */ + function _createTable() + { + $this->_db->loadModule('Manager', null, true); + $result = $this->_db->manager->createTable( + $this->_table, + array( + 'id' => array('type' => $this->_types['id']), + 'logtime' => array('type' => $this->_types['logtime']), + 'ident' => array('type' => $this->_types['ident']), + 'priority' => array('type' => $this->_types['priority']), + 'message' => array('type' => $this->_types['message']) + ) + ); + if (PEAR::isError($result)) { + return false; + } + + $result = $this->_db->manager->createIndex( + $this->_table, + 'unique_id', + array('fields' => array('id' => true), 'unique' => true) + ); + if (PEAR::isError($result)) { + return false; + } + + return true; + } + + /** + * Prepare the SQL insertion statement. + * + * @return boolean True if the statement was successfully created. + * + * @access private + * @since Log 1.9.0 + */ + function _prepareStatement() + { + $this->_statement = &$this->_db->prepare( + 'INSERT INTO ' . $this->_table . + ' (id, logtime, ident, priority, message)' . + ' VALUES(:id, :logtime, :ident, :priority, :message)', + $this->_types, MDB2_PREPARE_MANIP); + + /* Return success if we didn't generate an error. */ + return (PEAR::isError($this->_statement) === false); + } +} diff --git a/Log/null.php b/Log/null.php new file mode 100644 index 0000000..58cb9b4 --- /dev/null +++ b/Log/null.php @@ -0,0 +1,91 @@ + + * @since Log 1.8.2 + * @package Log + * + * @example null.php Using the null handler. + */ +class Log_null extends Log +{ + /** + * Constructs a new Log_null object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_null($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + } + + /** + * Opens the handler. + * + * @access public + * @since Log 1.9.6 + */ + function open() + { + $this->_opened = true; + return true; + } + + /** + * Closes the handler. + * + * @access public + * @since Log 1.9.6 + */ + function close() + { + $this->_opened = false; + return true; + } + + /** + * Simply consumes the log event. The message will still be passed + * along to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/Log/observer.php b/Log/observer.php new file mode 100644 index 0000000..d167ca9 --- /dev/null +++ b/Log/observer.php @@ -0,0 +1,129 @@ + + * @since Horde 1.3 + * @since Log 1.0 + * @package Log + * + * @example observer_mail.php An example Log_observer implementation. + */ +class Log_observer +{ + /** + * Instance-specific unique identification number. + * + * @var integer + * @access private + */ + var $_id = 0; + + /** + * The minimum priority level of message that we want to hear about. + * PEAR_LOG_EMERG is the highest priority, so we will only hear messages + * with an integer priority value less than or equal to ours. It defaults + * to PEAR_LOG_INFO, which listens to everything except PEAR_LOG_DEBUG. + * + * @var string + * @access private + */ + var $_priority = PEAR_LOG_INFO; + + /** + * Creates a new basic Log_observer instance. + * + * @param integer $priority The highest priority at which to receive + * log event notifications. + * + * @access public + */ + function Log_observer($priority = PEAR_LOG_INFO) + { + $this->_id = md5(microtime()); + $this->_priority = $priority; + } + + /** + * Attempts to return a new concrete Log_observer instance of the requested + * type. + * + * @param string $type The type of concreate Log_observer subclass + * to return. + * @param integer $priority The highest priority at which to receive + * log event notifications. + * @param array $conf Optional associative array of additional + * configuration values. + * + * @return object The newly created concrete Log_observer + * instance, or null on an error. + */ + function &factory($type, $priority = PEAR_LOG_INFO, $conf = array()) + { + $type = strtolower($type); + $class = 'Log_observer_' . $type; + + /* + * If the desired class already exists (because the caller has supplied + * it from some custom location), simply instantiate and return a new + * instance. + */ + if (class_exists($class)) { + $object = &new $class($priority, $conf); + return $object; + } + + /* Support both the new-style and old-style file naming conventions. */ + $newstyle = true; + $classfile = dirname(__FILE__) . '/observer_' . $type . '.php'; + + if (!file_exists($classfile)) { + $classfile = 'Log/' . $type . '.php'; + $newstyle = false; + } + + /* + * Attempt to include our version of the named class, but don't treat + * a failure as fatal. The caller may have already included their own + * version of the named class. + */ + @include_once $classfile; + + /* If the class exists, return a new instance of it. */ + if (class_exists($class)) { + /* Support both new-style and old-style construction. */ + if ($newstyle) { + $object = &new $class($priority, $conf); + } else { + $object = &new $class($priority); + } + return $object; + } + + $null = null; + return $null; + } + + /** + * This is a stub method to make sure that Log_Observer classes do + * something when they are notified of a message. The default behavior + * is to just print the message, which is obviously not desireable in + * practically any situation - which is why you need to override this + * method. :) + * + * @param array $event A hash describing the log event. + */ + function notify($event) + { + print_r($event); + } +} diff --git a/Log/sql.php b/Log/sql.php new file mode 100644 index 0000000..99f7a6d --- /dev/null +++ b/Log/sql.php @@ -0,0 +1,294 @@ + + * @since Horde 1.3 + * @since Log 1.0 + * @package Log + * + * @example sql.php Using the SQL handler. + */ +class Log_sql extends Log +{ + /** + * Variable containing the DSN information. + * @var mixed + * @access private + */ + var $_dsn = ''; + + /** + * String containing the SQL insertion statement. + * + * @var string + * @access private + */ + var $_sql = ''; + + /** + * Array containing our set of DB configuration options. + * @var array + * @access private + */ + var $_options = array('persistent' => true); + + /** + * Object holding the database handle. + * @var object + * @access private + */ + var $_db = null; + + /** + * Resource holding the prepared statement handle. + * @var resource + * @access private + */ + var $_statement = null; + + /** + * Flag indicating that we're using an existing database connection. + * @var boolean + * @access private + */ + var $_existingConnection = false; + + /** + * String holding the database table to use. + * @var string + * @access private + */ + var $_table = 'log_table'; + + /** + * String holding the name of the ID sequence. + * @var string + * @access private + */ + var $_sequence = 'log_id'; + + /** + * Maximum length of the $ident string. This corresponds to the size of + * the 'ident' column in the SQL table. + * @var integer + * @access private + */ + var $_identLimit = 16; + + + /** + * Constructs a new sql logging object. + * + * @param string $name The target SQL table. + * @param string $ident The identification field. + * @param array $conf The connection configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_sql($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_table = $name; + $this->_mask = Log::UPTO($level); + + /* Now that we have a table name, assign our SQL statement. */ + if (!empty($this->_sql)) { + $this->_sql = $conf['sql']; + } else { + $this->_sql = 'INSERT INTO ' . $this->_table . + ' (id, logtime, ident, priority, message)' . + ' VALUES(?, CURRENT_TIMESTAMP, ?, ?, ?)'; + } + + /* If an options array was provided, use it. */ + if (isset($conf['options']) && is_array($conf['options'])) { + $this->_options = $conf['options']; + } + + /* If a specific sequence name was provided, use it. */ + if (!empty($conf['sequence'])) { + $this->_sequence = $conf['sequence']; + } + + /* If a specific sequence name was provided, use it. */ + if (isset($conf['identLimit'])) { + $this->_identLimit = $conf['identLimit']; + } + + /* Now that the ident limit is confirmed, set the ident string. */ + $this->setIdent($ident); + + /* If an existing database connection was provided, use it. */ + if (isset($conf['db'])) { + $this->_db = &$conf['db']; + $this->_existingConnection = true; + $this->_opened = true; + } else { + $this->_dsn = $conf['dsn']; + } + } + + /** + * Opens a connection to the database, if it has not already + * been opened. This is implicitly called by log(), if necessary. + * + * @return boolean True on success, false on failure. + * @access public + */ + function open() + { + if (!$this->_opened) { + /* Use the DSN and options to create a database connection. */ + $this->_db = &DB::connect($this->_dsn, $this->_options); + if (DB::isError($this->_db)) { + return false; + } + + /* Create a prepared statement for repeated use in log(). */ + if (!$this->_prepareStatement()) { + return false; + } + + /* We now consider out connection open. */ + $this->_opened = true; + } + + return $this->_opened; + } + + /** + * Closes the connection to the database if it is still open and we were + * the ones that opened it. It is the caller's responsible to close an + * existing connection that was passed to us via $conf['db']. + * + * @return boolean True on success, false on failure. + * @access public + */ + function close() + { + if ($this->_opened && !$this->_existingConnection) { + $this->_opened = false; + $this->_db->freePrepared($this->_statement); + return $this->_db->disconnect(); + } + + return ($this->_opened === false); + } + + /** + * Sets this Log instance's identification string. Note that this + * SQL-specific implementation will limit the length of the $ident string + * to sixteen (16) characters. + * + * @param string $ident The new identification string. + * + * @access public + * @since Log 1.8.5 + */ + function setIdent($ident) + { + $this->_ident = substr($ident, 0, $this->_identLimit); + } + + /** + * Inserts $message to the currently open database. Calls open(), + * if necessary. Also passes the message along to any Log_observer + * instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* If we don't already have our statement object yet, create it. */ + if (!is_object($this->_statement) && !$this->_prepareStatement()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build our set of values for this log entry. */ + $id = $this->_db->nextId($this->_sequence); + $values = array($id, $this->_ident, $priority, $message); + + /* Execute the SQL query for this log entry insertion. */ + $result =& $this->_db->execute($this->_statement, $values); + if (DB::isError($result)) { + return false; + } + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + + /** + * Prepare the SQL insertion statement. + * + * @return boolean True if the statement was successfully created. + * + * @access private + * @since Log 1.9.1 + */ + function _prepareStatement() + { + $this->_statement = $this->_db->prepare($this->_sql); + + /* Return success if we didn't generate an error. */ + return (DB::isError($this->_statement) === false); + } +} diff --git a/Log/sqlite.php b/Log/sqlite.php new file mode 100644 index 0000000..a558fee --- /dev/null +++ b/Log/sqlite.php @@ -0,0 +1,225 @@ + + * @author Jon Parise + * @since Log 1.8.3 + * @package Log + * + * @example sqlite.php Using the Sqlite handler. + */ +class Log_sqlite extends Log +{ + /** + * Array containing the connection defaults + * @var array + * @access private + */ + var $_options = array('mode' => 0666, + 'persistent' => false); + + /** + * Object holding the database handle. + * @var object + * @access private + */ + var $_db = null; + + /** + * Flag indicating that we're using an existing database connection. + * @var boolean + * @access private + */ + var $_existingConnection = false; + + /** + * String holding the database table to use. + * @var string + * @access private + */ + var $_table = 'log_table'; + + + /** + * Constructs a new sql logging object. + * + * @param string $name The target SQL table. + * @param string $ident The identification field. + * @param mixed $conf Can be an array of configuration options used + * to open a new database connection + * or an already opened sqlite connection. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_sqlite($name, $ident = '', &$conf, $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_table = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (is_array($conf)) { + foreach ($conf as $k => $opt) { + $this->_options[$k] = $opt; + } + } else { + // If an existing database connection was provided, use it. + $this->_db =& $conf; + $this->_existingConnection = true; + } + } + + /** + * Opens a connection to the database, if it has not already + * been opened. This is implicitly called by log(), if necessary. + * + * @return boolean True on success, false on failure. + * @access public + */ + function open() + { + if (is_resource($this->_db)) { + $this->_opened = true; + return $this->_createTable(); + } else { + /* Set the connection function based on the 'persistent' option. */ + if (empty($this->_options['persistent'])) { + $connectFunction = 'sqlite_open'; + } else { + $connectFunction = 'sqlite_popen'; + } + + /* Attempt to connect to the database. */ + if ($this->_db = $connectFunction($this->_options['filename'], + (int)$this->_options['mode'], + $error)) { + $this->_opened = true; + return $this->_createTable(); + } + } + + return $this->_opened; + } + + /** + * Closes the connection to the database if it is still open and we were + * the ones that opened it. It is the caller's responsible to close an + * existing connection that was passed to us via $conf['db']. + * + * @return boolean True on success, false on failure. + * @access public + */ + function close() + { + /* We never close existing connections. */ + if ($this->_existingConnection) { + return false; + } + + if ($this->_opened) { + $this->_opened = false; + sqlite_close($this->_db); + } + + return ($this->_opened === false); + } + + /** + * Inserts $message to the currently open database. Calls open(), + * if necessary. Also passes the message along to any Log_observer + * instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + // Extract the string representation of the message. + $message = $this->_extractMessage($message); + + // Build the SQL query for this log entry insertion. + $q = sprintf('INSERT INTO [%s] (logtime, ident, priority, message) ' . + "VALUES ('%s', '%s', %d, '%s')", + $this->_table, + strftime('%Y-%m-%d %H:%M:%S', time()), + sqlite_escape_string($this->_ident), + $priority, + sqlite_escape_string($message)); + if (!($res = @sqlite_unbuffered_query($this->_db, $q))) { + return false; + } + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + + /** + * Checks whether the log table exists and creates it if necessary. + * + * @return boolean True on success or false on failure. + * @access private + */ + function _createTable() + { + $q = "SELECT name FROM sqlite_master WHERE name='" . $this->_table . + "' AND type='table'"; + + $res = sqlite_query($this->_db, $q); + + if (sqlite_num_rows($res) == 0) { + $q = 'CREATE TABLE [' . $this->_table . '] (' . + 'id INTEGER PRIMARY KEY NOT NULL, ' . + 'logtime NOT NULL, ' . + 'ident CHAR(16) NOT NULL, ' . + 'priority INT NOT NULL, ' . + 'message)'; + + if (!($res = sqlite_unbuffered_query($this->_db, $q))) { + return false; + } + } + + return true; + } + +} diff --git a/Log/syslog.php b/Log/syslog.php new file mode 100644 index 0000000..22af4b6 --- /dev/null +++ b/Log/syslog.php @@ -0,0 +1,179 @@ + + * @author Jon Parise + * @since Horde 1.3 + * @since Log 1.0 + * @package Log + * + * @example syslog.php Using the syslog handler. + */ +class Log_syslog extends Log +{ + /** + * Integer holding the log facility to use. + * @var integer + * @access private + */ + var $_name = LOG_SYSLOG; + + /** + * Should we inherit the current syslog connection for this process, or + * should we call openlog() to start a new syslog connection? + * @var boolean + * @access private + */ + var $_inherit = false; + + /** + * Constructs a new syslog object. + * + * @param string $name The syslog facility. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_syslog($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + /* Ensure we have a valid integer value for $name. */ + if (empty($name) || !is_int($name)) { + $name = LOG_SYSLOG; + } + + if (isset($conf['inherit'])) { + $this->_inherit = $conf['inherit']; + $this->_opened = $this->_inherit; + } + + $this->_id = md5(microtime()); + $this->_name = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + } + + /** + * Opens a connection to the system logger, if it has not already + * been opened. This is implicitly called by log(), if necessary. + * @access public + */ + function open() + { + if (!$this->_opened) { + $this->_opened = openlog($this->_ident, LOG_PID, $this->_name); + } + + return $this->_opened; + } + + /** + * Closes the connection to the system logger, if it is open. + * @access public + */ + function close() + { + if ($this->_opened && !$this->_inherit) { + closelog(); + $this->_opened = false; + } + + return true; + } + + /** + * Sends $message to the currently open syslog connection. Calls + * open() if necessary. Also passes the message along to any Log_observer + * instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param int $priority (optional) The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* If the connection isn't open and can't be opened, return failure. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + + /* Build a syslog priority value based on our current configuration. */ + $priority = $this->_toSyslog($priority); + if ($this->_inherit) { + $priority |= $this->_name; + } + + if (!syslog($priority, $message)) { + return false; + } + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + + /** + * Converts a PEAR_LOG_* constant into a syslog LOG_* constant. + * + * This function exists because, under Windows, not all of the LOG_* + * constants have unique values. Instead, the PEAR_LOG_* were introduced + * for global use, with the conversion to the LOG_* constants kept local to + * to the syslog driver. + * + * @param int $priority PEAR_LOG_* value to convert to LOG_* value. + * + * @return The LOG_* representation of $priority. + * + * @access private + */ + function _toSyslog($priority) + { + static $priorities = array( + PEAR_LOG_EMERG => LOG_EMERG, + PEAR_LOG_ALERT => LOG_ALERT, + PEAR_LOG_CRIT => LOG_CRIT, + PEAR_LOG_ERR => LOG_ERR, + PEAR_LOG_WARNING => LOG_WARNING, + PEAR_LOG_NOTICE => LOG_NOTICE, + PEAR_LOG_INFO => LOG_INFO, + PEAR_LOG_DEBUG => LOG_DEBUG + ); + + /* If we're passed an unknown priority, default to LOG_INFO. */ + if (!is_int($priority) || !in_array($priority, $priorities)) { + return LOG_INFO; + } + + return $priorities[$priority]; + } + +} diff --git a/Log/win.php b/Log/win.php new file mode 100644 index 0000000..21795d9 --- /dev/null +++ b/Log/win.php @@ -0,0 +1,269 @@ + + * @since Log 1.7.0 + * @package Log + * + * @example win.php Using the window handler. + */ +class Log_win extends Log +{ + /** + * The name of the output window. + * @var string + * @access private + */ + var $_name = 'LogWindow'; + + /** + * The title of the output window. + * @var string + * @access private + */ + var $_title = 'Log Output Window'; + + /** + * Mapping of log priorities to styles. + * @var array + * @access private + */ + var $_styles = array( + PEAR_LOG_EMERG => 'color: red;', + PEAR_LOG_ALERT => 'color: orange;', + PEAR_LOG_CRIT => 'color: yellow;', + PEAR_LOG_ERR => 'color: green;', + PEAR_LOG_WARNING => 'color: blue;', + PEAR_LOG_NOTICE => 'color: indigo;', + PEAR_LOG_INFO => 'color: violet;', + PEAR_LOG_DEBUG => 'color: black;' + ); + + /** + * String buffer that holds line that are pending output. + * @var array + * @access private + */ + var $_buffer = array(); + + /** + * Constructs a new Log_win object. + * + * @param string $name Ignored. + * @param string $ident The identity string. + * @param array $conf The configuration array. + * @param int $level Log messages up to and including this level. + * @access public + */ + function Log_win($name, $ident = '', $conf = array(), + $level = PEAR_LOG_DEBUG) + { + $this->_id = md5(microtime()); + $this->_name = $name; + $this->_ident = $ident; + $this->_mask = Log::UPTO($level); + + if (isset($conf['title'])) { + $this->_title = $conf['title']; + } + if (isset($conf['styles']) && is_array($conf['styles'])) { + $this->_styles = $conf['styles']; + } + if (isset($conf['colors']) && is_array($conf['colors'])) { + foreach ($conf['colors'] as $level => $color) { + $this->_styles[$level] .= "color: $color;"; + } + } + + register_shutdown_function(array(&$this, '_Log_win')); + } + + /** + * Destructor + */ + function _Log_win() + { + if ($this->_opened || (count($this->_buffer) > 0)) { + $this->close(); + } + } + + /** + * The first time open() is called, it will open a new browser window and + * prepare it for output. + * + * This is implicitly called by log(), if necessary. + * + * @access public + */ + function open() + { + if (!$this->_opened) { + $win = $this->_name; + $styles = $this->_styles; + + if (!empty($this->_ident)) { + $identHeader = "$win.document.writeln('Ident')"; + } else { + $identHeader = ''; + } + + echo <<< EOT + +EOT; + $this->_opened = true; + } + + return $this->_opened; + } + + /** + * Closes the output stream if it is open. If there are still pending + * lines in the output buffer, the output window will be opened so that + * the buffer can be drained. + * + * @access public + */ + function close() + { + /* + * If there are still lines waiting to be written, open the output + * window so that we can drain the buffer. + */ + if (!$this->_opened && (count($this->_buffer) > 0)) { + $this->open(); + } + + if ($this->_opened) { + $this->_writeln(''); + $this->_writeln(''); + $this->_opened = false; + } + + return ($this->_opened === false); + } + + /** + * Writes a single line of text to the output window. + * + * @param string $line The line of text to write. + * + * @access private + */ + function _writeln($line) + { + /* Add this line to our output buffer. */ + $this->_buffer[] = $line; + + /* Buffer the output until this page's headers have been sent. */ + if (!headers_sent()) { + return; + } + + /* If we haven't already opened the output window, do so now. */ + if (!$this->_opened && !$this->open()) { + return false; + } + + /* Drain the buffer to the output window. */ + $win = $this->_name; + foreach ($this->_buffer as $line) { + echo "\n"; + } + + /* Now that the buffer has been drained, clear it. */ + $this->_buffer = array(); + } + + /** + * Logs $message to the output window. The message is also passed along + * to any Log_observer instances that are observing this Log. + * + * @param mixed $message String or object containing the message to log. + * @param string $priority The priority of the message. Valid + * values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT, + * PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING, + * PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG. + * @return boolean True on success or false on failure. + * @access public + */ + function log($message, $priority = null) + { + /* If a priority hasn't been specified, use the default value. */ + if ($priority === null) { + $priority = $this->_priority; + } + + /* Abort early if the priority is above the maximum logging level. */ + if (!$this->_isMasked($priority)) { + return false; + } + + /* Extract the string representation of the message. */ + $message = $this->_extractMessage($message); + $message = preg_replace('/\r\n|\n|\r/', '
', $message); + + list($usec, $sec) = explode(' ', microtime()); + + /* Build the output line that contains the log entry row. */ + $line = ''; + $line .= sprintf('%s.%s', + strftime('%H:%M:%S', $sec), substr($usec, 2, 2)); + if (!empty($this->_ident)) { + $line .= '' . $this->_ident . ''; + } + $line .= '' . ucfirst($this->priorityToString($priority)) . ''; + $line .= sprintf('%s', $priority, $message); + $line .= ''; + + $this->_writeln($line); + + $this->_announce(array('priority' => $priority, 'message' => $message)); + + return true; + } + +} diff --git a/MDB2.php b/MDB2.php new file mode 100644 index 0000000..e0ca3d8 --- /dev/null +++ b/MDB2.php @@ -0,0 +1,4271 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: MDB2.php,v 1.292 2007/04/25 09:31:01 quipo Exp $ +// + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +require_once 'PEAR.php'; + +// {{{ Error constants + +/** + * The method mapErrorCode in each MDB2_dbtype implementation maps + * native error codes to one of these. + * + * If you add an error code here, make sure you also add a textual + * version of it in MDB2::errorMessage(). + */ + +define('MDB2_OK', true); +define('MDB2_ERROR', -1); +define('MDB2_ERROR_SYNTAX', -2); +define('MDB2_ERROR_CONSTRAINT', -3); +define('MDB2_ERROR_NOT_FOUND', -4); +define('MDB2_ERROR_ALREADY_EXISTS', -5); +define('MDB2_ERROR_UNSUPPORTED', -6); +define('MDB2_ERROR_MISMATCH', -7); +define('MDB2_ERROR_INVALID', -8); +define('MDB2_ERROR_NOT_CAPABLE', -9); +define('MDB2_ERROR_TRUNCATED', -10); +define('MDB2_ERROR_INVALID_NUMBER', -11); +define('MDB2_ERROR_INVALID_DATE', -12); +define('MDB2_ERROR_DIVZERO', -13); +define('MDB2_ERROR_NODBSELECTED', -14); +define('MDB2_ERROR_CANNOT_CREATE', -15); +define('MDB2_ERROR_CANNOT_DELETE', -16); +define('MDB2_ERROR_CANNOT_DROP', -17); +define('MDB2_ERROR_NOSUCHTABLE', -18); +define('MDB2_ERROR_NOSUCHFIELD', -19); +define('MDB2_ERROR_NEED_MORE_DATA', -20); +define('MDB2_ERROR_NOT_LOCKED', -21); +define('MDB2_ERROR_VALUE_COUNT_ON_ROW', -22); +define('MDB2_ERROR_INVALID_DSN', -23); +define('MDB2_ERROR_CONNECT_FAILED', -24); +define('MDB2_ERROR_EXTENSION_NOT_FOUND',-25); +define('MDB2_ERROR_NOSUCHDB', -26); +define('MDB2_ERROR_ACCESS_VIOLATION', -27); +define('MDB2_ERROR_CANNOT_REPLACE', -28); +define('MDB2_ERROR_CONSTRAINT_NOT_NULL',-29); +define('MDB2_ERROR_DEADLOCK', -30); +define('MDB2_ERROR_CANNOT_ALTER', -31); +define('MDB2_ERROR_MANAGER', -32); +define('MDB2_ERROR_MANAGER_PARSE', -33); +define('MDB2_ERROR_LOADMODULE', -34); +define('MDB2_ERROR_INSUFFICIENT_DATA', -35); +// }}} +// {{{ Verbose constants +/** + * These are just helper constants to more verbosely express parameters to prepare() + */ + +define('MDB2_PREPARE_MANIP', false); +define('MDB2_PREPARE_RESULT', null); + +// }}} +// {{{ Fetchmode constants + +/** + * This is a special constant that tells MDB2 the user hasn't specified + * any particular get mode, so the default should be used. + */ +define('MDB2_FETCHMODE_DEFAULT', 0); + +/** + * Column data indexed by numbers, ordered from 0 and up + */ +define('MDB2_FETCHMODE_ORDERED', 1); + +/** + * Column data indexed by column names + */ +define('MDB2_FETCHMODE_ASSOC', 2); + +/** + * Column data as object properties + */ +define('MDB2_FETCHMODE_OBJECT', 3); + +/** + * For multi-dimensional results: normally the first level of arrays + * is the row number, and the second level indexed by column number or name. + * MDB2_FETCHMODE_FLIPPED switches this order, so the first level of arrays + * is the column name, and the second level the row number. + */ +define('MDB2_FETCHMODE_FLIPPED', 4); + +// }}} +// {{{ Portability mode constants + +/** + * Portability: turn off all portability features. + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_NONE', 0); + +/** + * Portability: convert names of tables and fields to case defined in the + * "field_case" option when using the query*(), fetch*() and tableInfo() methods. + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_FIX_CASE', 1); + +/** + * Portability: right trim the data output by query*() and fetch*(). + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_RTRIM', 2); + +/** + * Portability: force reporting the number of rows deleted. + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_DELETE_COUNT', 4); + +/** + * Portability: not needed in MDB2 (just left here for compatibility to DB) + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_NUMROWS', 8); + +/** + * Portability: makes certain error messages in certain drivers compatible + * with those from other DBMS's. + * + * + mysql, mysqli: change unique/primary key constraints + * MDB2_ERROR_ALREADY_EXISTS -> MDB2_ERROR_CONSTRAINT + * + * + odbc(access): MS's ODBC driver reports 'no such field' as code + * 07001, which means 'too few parameters.' When this option is on + * that code gets mapped to MDB2_ERROR_NOSUCHFIELD. + * + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_ERRORS', 16); + +/** + * Portability: convert empty values to null strings in data output by + * query*() and fetch*(). + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_EMPTY_TO_NULL', 32); + +/** + * Portability: removes database/table qualifiers from associative indexes + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES', 64); + +/** + * Portability: turn on all portability features. + * @see MDB2_Driver_Common::setOption() + */ +define('MDB2_PORTABILITY_ALL', 127); + +// }}} +// {{{ Globals for class instance tracking + +/** + * These are global variables that are used to track the various class instances + */ + +$GLOBALS['_MDB2_databases'] = array(); +$GLOBALS['_MDB2_dsninfo_default'] = array( + 'phptype' => false, + 'dbsyntax' => false, + 'username' => false, + 'password' => false, + 'protocol' => false, + 'hostspec' => false, + 'port' => false, + 'socket' => false, + 'database' => false, + 'mode' => false, +); + +// }}} +// {{{ class MDB2 + +/** + * The main 'MDB2' class is simply a container class with some static + * methods for creating DB objects as well as some utility functions + * common to all parts of DB. + * + * The object model of MDB2 is as follows (indentation means inheritance): + * + * MDB2 The main MDB2 class. This is simply a utility class + * with some 'static' methods for creating MDB2 objects as + * well as common utility functions for other MDB2 classes. + * + * MDB2_Driver_Common The base for each MDB2 implementation. Provides default + * | implementations (in OO lingo virtual methods) for + * | the actual DB implementations as well as a bunch of + * | query utility functions. + * | + * +-MDB2_Driver_mysql The MDB2 implementation for MySQL. Inherits MDB2_Driver_Common. + * When calling MDB2::factory or MDB2::connect for MySQL + * connections, the object returned is an instance of this + * class. + * +-MDB2_Driver_pgsql The MDB2 implementation for PostGreSQL. Inherits MDB2_Driver_Common. + * When calling MDB2::factory or MDB2::connect for PostGreSQL + * connections, the object returned is an instance of this + * class. + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2 +{ + // {{{ function setOptions(&$db, $options) + + /** + * set option array in an exiting database object + * + * @param MDB2_Driver_Common MDB2 object + * @param array An associative array of option names and their values. + * + * @return mixed MDB2_OK or a PEAR Error object + * + * @access public + */ + function setOptions(&$db, $options) + { + if (is_array($options)) { + foreach ($options as $option => $value) { + $test = $db->setOption($option, $value); + if (PEAR::isError($test)) { + return $test; + } + } + } + return MDB2_OK; + } + + // }}} + // {{{ function classExists($classname) + + /** + * Checks if a class exists without triggering __autoload + * + * @param string classname + * + * @return bool true success and false on error + * @static + * @access public + */ + function classExists($classname) + { + if (version_compare(phpversion(), "5.0", ">=")) { + return class_exists($classname, false); + } + return class_exists($classname); + } + + // }}} + // {{{ function loadClass($class_name, $debug) + + /** + * Loads a PEAR class. + * + * @param string classname to load + * @param bool if errors should be suppressed + * + * @return mixed true success or PEAR_Error on failure + * + * @access public + */ + function loadClass($class_name, $debug) + { + if (!MDB2::classExists($class_name)) { + $file_name = str_replace('_', DIRECTORY_SEPARATOR, $class_name).'.php'; + if ($debug) { + $include = include_once($file_name); + } else { + $include = @include_once($file_name); + } + if (!$include) { + if (!MDB2::fileExists($file_name)) { + $msg = "unable to find package '$class_name' file '$file_name'"; + } else { + $msg = "unable to load class '$class_name' from file '$file_name'"; + } + $err =& MDB2::raiseError(MDB2_ERROR_NOT_FOUND, null, null, $msg); + return $err; + } + } + return MDB2_OK; + } + + // }}} + // {{{ function &factory($dsn, $options = false) + + /** + * Create a new MDB2 object for the specified database type + * + * IMPORTANT: In order for MDB2 to work properly it is necessary that + * you make sure that you work with a reference of the original + * object instead of a copy (this is a PHP4 quirk). + * + * For example: + * $db =& MDB2::factory($dsn); + * ^^ + * And not: + * $db = MDB2::factory($dsn); + * + * @param mixed 'data source name', see the MDB2::parseDSN + * method for a description of the dsn format. + * Can also be specified as an array of the + * format returned by MDB2::parseDSN. + * @param array An associative array of option names and + * their values. + * + * @return mixed a newly created MDB2 object, or false on error + * + * @access public + */ + function &factory($dsn, $options = false) + { + $dsninfo = MDB2::parseDSN($dsn); + if (empty($dsninfo['phptype'])) { + $err =& MDB2::raiseError(MDB2_ERROR_NOT_FOUND, + null, null, 'no RDBMS driver specified'); + return $err; + } + $class_name = 'MDB2_Driver_'.$dsninfo['phptype']; + + $debug = (!empty($options['debug'])); + $err = MDB2::loadClass($class_name, $debug); + if (PEAR::isError($err)) { + return $err; + } + + $db =& new $class_name(); + $db->setDSN($dsninfo); + $err = MDB2::setOptions($db, $options); + if (PEAR::isError($err)) { + return $err; + } + + return $db; + } + + // }}} + // {{{ function &connect($dsn, $options = false) + + /** + * Create a new MDB2 connection object and connect to the specified + * database + * + * IMPORTANT: In order for MDB2 to work properly it is necessary that + * you make sure that you work with a reference of the original + * object instead of a copy (this is a PHP4 quirk). + * + * For example: + * $db =& MDB2::connect($dsn); + * ^^ + * And not: + * $db = MDB2::connect($dsn); + * ^^ + * + * @param mixed 'data source name', see the MDB2::parseDSN + * method for a description of the dsn format. + * Can also be specified as an array of the + * format returned by MDB2::parseDSN. + * @param array An associative array of option names and + * their values. + * + * @return mixed a newly created MDB2 connection object, or a MDB2 + * error object on error + * + * @access public + * @see MDB2::parseDSN + */ + function &connect($dsn, $options = false) + { + $db =& MDB2::factory($dsn, $options); + if (PEAR::isError($db)) { + return $db; + } + + $err = $db->connect(); + if (PEAR::isError($err)) { + $dsn = $db->getDSN('string', 'xxx'); + $db->disconnect(); + $err->addUserInfo($dsn); + return $err; + } + + return $db; + } + + // }}} + // {{{ function &singleton($dsn = null, $options = false) + + /** + * Returns a MDB2 connection with the requested DSN. + * A new MDB2 connection object is only created if no object with the + * requested DSN exists yet. + * + * IMPORTANT: In order for MDB2 to work properly it is necessary that + * you make sure that you work with a reference of the original + * object instead of a copy (this is a PHP4 quirk). + * + * For example: + * $db =& MDB2::singleton($dsn); + * ^^ + * And not: + * $db = MDB2::singleton($dsn); + * ^^ + * + * @param mixed 'data source name', see the MDB2::parseDSN + * method for a description of the dsn format. + * Can also be specified as an array of the + * format returned by MDB2::parseDSN. + * @param array An associative array of option names and + * their values. + * + * @return mixed a newly created MDB2 connection object, or a MDB2 + * error object on error + * + * @access public + * @see MDB2::parseDSN + */ + function &singleton($dsn = null, $options = false) + { + if ($dsn) { + $dsninfo = MDB2::parseDSN($dsn); + $dsninfo = array_merge($GLOBALS['_MDB2_dsninfo_default'], $dsninfo); + $keys = array_keys($GLOBALS['_MDB2_databases']); + for ($i=0, $j=count($keys); $i<$j; ++$i) { + if (isset($GLOBALS['_MDB2_databases'][$keys[$i]])) { + $tmp_dsn = $GLOBALS['_MDB2_databases'][$keys[$i]]->getDSN('array'); + if (count(array_diff_assoc($tmp_dsn, $dsninfo)) == 0) { + MDB2::setOptions($GLOBALS['_MDB2_databases'][$keys[$i]], $options); + return $GLOBALS['_MDB2_databases'][$keys[$i]]; + } + } + } + } elseif (is_array($GLOBALS['_MDB2_databases']) && reset($GLOBALS['_MDB2_databases'])) { + $db =& $GLOBALS['_MDB2_databases'][key($GLOBALS['_MDB2_databases'])]; + return $db; + } + $db =& MDB2::factory($dsn, $options); + return $db; + } + + // }}} + // {{{ function loadFile($file) + + /** + * load a file (like 'Date') + * + * @param string name of the file in the MDB2 directory (without '.php') + * + * @return string name of the file that was included + * + * @access public + */ + function loadFile($file) + { + $file_name = 'MDB2'.DIRECTORY_SEPARATOR.$file.'.php'; + if (!MDB2::fileExists($file_name)) { + return MDB2::raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'unable to find: '.$file_name); + } + if (!include_once($file_name)) { + return MDB2::raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'unable to load driver class: '.$file_name); + } + return $file_name; + } + + // }}} + // {{{ function apiVersion() + + /** + * Return the MDB2 API version + * + * @return string the MDB2 API version number + * + * @access public + */ + function apiVersion() + { + return '2.4.1'; + } + + // }}} + // {{{ function &raiseError($code = null, $mode = null, $options = null, $userinfo = null) + + /** + * This method is used to communicate an error and invoke error + * callbacks etc. Basically a wrapper for PEAR::raiseError + * without the message string. + * + * @param mixed int error code + * + * @param int error mode, see PEAR_Error docs + * + * @param mixed If error mode is PEAR_ERROR_TRIGGER, this is the + * error level (E_USER_NOTICE etc). If error mode is + * PEAR_ERROR_CALLBACK, this is the callback function, + * either as a function name, or as an array of an + * object and method name. For other error modes this + * parameter is ignored. + * + * @param string Extra debug information. Defaults to the last + * query and native error code. + * + * @return PEAR_Error instance of a PEAR Error object + * + * @access private + * @see PEAR_Error + */ + function &raiseError($code = null, $mode = null, $options = null, $userinfo = null) + { + $err =& PEAR::raiseError(null, $code, $mode, $options, $userinfo, 'MDB2_Error', true); + return $err; + } + + // }}} + // {{{ function isError($data, $code = null) + + /** + * Tell whether a value is a MDB2 error. + * + * @param mixed the value to test + * @param int if is an error object, return true + * only if $code is a string and + * $db->getMessage() == $code or + * $code is an integer and $db->getCode() == $code + * + * @return bool true if parameter is an error + * + * @access public + */ + function isError($data, $code = null) + { + if (is_a($data, 'MDB2_Error')) { + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() === $code; + } else { + $code = (array)$code; + return in_array($data->getCode(), $code); + } + } + return false; + } + + // }}} + // {{{ function isConnection($value) + + /** + * Tell whether a value is a MDB2 connection + * + * @param mixed value to test + * + * @return bool whether $value is a MDB2 connection + * + * @access public + */ + function isConnection($value) + { + return is_a($value, 'MDB2_Driver_Common'); + } + + // }}} + // {{{ function isResult($value) + + /** + * Tell whether a value is a MDB2 result + * + * @param mixed value to test + * + * @return bool whether $value is a MDB2 result + * + * @access public + */ + function isResult($value) + { + return is_a($value, 'MDB2_Result'); + } + + // }}} + // {{{ function isResultCommon($value) + + /** + * Tell whether a value is a MDB2 result implementing the common interface + * + * @param mixed value to test + * + * @return bool whether $value is a MDB2 result implementing the common interface + * + * @access public + */ + function isResultCommon($value) + { + return is_a($value, 'MDB2_Result_Common'); + } + + // }}} + // {{{ function isStatement($value) + + /** + * Tell whether a value is a MDB2 statement interface + * + * @param mixed value to test + * + * @return bool whether $value is a MDB2 statement interface + * + * @access public + */ + function isStatement($value) + { + return is_a($value, 'MDB2_Statement'); + } + + // }}} + // {{{ function errorMessage($value = null) + + /** + * Return a textual error message for a MDB2 error code + * + * @param int|array integer error code, + null to get the current error code-message map, + or an array with a new error code-message map + * + * @return string error message, or false if the error code was + * not recognized + * + * @access public + */ + function errorMessage($value = null) + { + static $errorMessages; + + if (is_array($value)) { + $errorMessages = $value; + return MDB2_OK; + } + + if (!isset($errorMessages)) { + $errorMessages = array( + MDB2_OK => 'no error', + MDB2_ERROR => 'unknown error', + MDB2_ERROR_ALREADY_EXISTS => 'already exists', + MDB2_ERROR_CANNOT_CREATE => 'can not create', + MDB2_ERROR_CANNOT_ALTER => 'can not alter', + MDB2_ERROR_CANNOT_REPLACE => 'can not replace', + MDB2_ERROR_CANNOT_DELETE => 'can not delete', + MDB2_ERROR_CANNOT_DROP => 'can not drop', + MDB2_ERROR_CONSTRAINT => 'constraint violation', + MDB2_ERROR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint', + MDB2_ERROR_DIVZERO => 'division by zero', + MDB2_ERROR_INVALID => 'invalid', + MDB2_ERROR_INVALID_DATE => 'invalid date or time', + MDB2_ERROR_INVALID_NUMBER => 'invalid number', + MDB2_ERROR_MISMATCH => 'mismatch', + MDB2_ERROR_NODBSELECTED => 'no database selected', + MDB2_ERROR_NOSUCHFIELD => 'no such field', + MDB2_ERROR_NOSUCHTABLE => 'no such table', + MDB2_ERROR_NOT_CAPABLE => 'MDB2 backend not capable', + MDB2_ERROR_NOT_FOUND => 'not found', + MDB2_ERROR_NOT_LOCKED => 'not locked', + MDB2_ERROR_SYNTAX => 'syntax error', + MDB2_ERROR_UNSUPPORTED => 'not supported', + MDB2_ERROR_VALUE_COUNT_ON_ROW => 'value count on row', + MDB2_ERROR_INVALID_DSN => 'invalid DSN', + MDB2_ERROR_CONNECT_FAILED => 'connect failed', + MDB2_ERROR_NEED_MORE_DATA => 'insufficient data supplied', + MDB2_ERROR_EXTENSION_NOT_FOUND=> 'extension not found', + MDB2_ERROR_NOSUCHDB => 'no such database', + MDB2_ERROR_ACCESS_VIOLATION => 'insufficient permissions', + MDB2_ERROR_LOADMODULE => 'error while including on demand module', + MDB2_ERROR_TRUNCATED => 'truncated', + MDB2_ERROR_DEADLOCK => 'deadlock detected', + ); + } + + if (is_null($value)) { + return $errorMessages; + } + + if (PEAR::isError($value)) { + $value = $value->getCode(); + } + + return isset($errorMessages[$value]) ? + $errorMessages[$value] : $errorMessages[MDB2_ERROR]; + } + + // }}} + // {{{ function parseDSN($dsn) + + /** + * Parse a data source name. + * + * Additional keys can be added by appending a URI query string to the + * end of the DSN. + * + * The format of the supplied DSN is in its fullest form: + * + * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true + * + * + * Most variations are allowed: + * + * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644 + * phptype://username:password@hostspec/database_name + * phptype://username:password@hostspec + * phptype://username@hostspec + * phptype://hostspec/database + * phptype://hostspec + * phptype(dbsyntax) + * phptype + * + * + * @param string Data Source Name to be parsed + * + * @return array an associative array with the following keys: + * + phptype: Database backend used in PHP (mysql, odbc etc.) + * + dbsyntax: Database used with regards to SQL syntax etc. + * + protocol: Communication protocol to use (tcp, unix etc.) + * + hostspec: Host specification (hostname[:port]) + * + database: Database to use on the DBMS server + * + username: User name for login + * + password: Password for login + * + * @access public + * @author Tomas V.V.Cox + */ + function parseDSN($dsn) + { + $parsed = $GLOBALS['_MDB2_dsninfo_default']; + + if (is_array($dsn)) { + $dsn = array_merge($parsed, $dsn); + if (!$dsn['dbsyntax']) { + $dsn['dbsyntax'] = $dsn['phptype']; + } + return $dsn; + } + + // Find phptype and dbsyntax + if (($pos = strpos($dsn, '://')) !== false) { + $str = substr($dsn, 0, $pos); + $dsn = substr($dsn, $pos + 3); + } else { + $str = $dsn; + $dsn = null; + } + + // Get phptype and dbsyntax + // $str => phptype(dbsyntax) + if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) { + $parsed['phptype'] = $arr[1]; + $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2]; + } else { + $parsed['phptype'] = $str; + $parsed['dbsyntax'] = $str; + } + + if (!count($dsn)) { + return $parsed; + } + + // Get (if found): username and password + // $dsn => username:password@protocol+hostspec/database + if (($at = strrpos($dsn,'@')) !== false) { + $str = substr($dsn, 0, $at); + $dsn = substr($dsn, $at + 1); + if (($pos = strpos($str, ':')) !== false) { + $parsed['username'] = rawurldecode(substr($str, 0, $pos)); + $parsed['password'] = rawurldecode(substr($str, $pos + 1)); + } else { + $parsed['username'] = rawurldecode($str); + } + } + + // Find protocol and hostspec + + // $dsn => proto(proto_opts)/database + if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) { + $proto = $match[1]; + $proto_opts = $match[2] ? $match[2] : false; + $dsn = $match[3]; + + // $dsn => protocol+hostspec/database (old format) + } else { + if (strpos($dsn, '+') !== false) { + list($proto, $dsn) = explode('+', $dsn, 2); + } + if ( strpos($dsn, '//') === 0 + && strpos($dsn, '/', 2) !== false + && $parsed['phptype'] == 'oci8' + ) { + //oracle's "Easy Connect" syntax: + //"username/password@[//]host[:port][/service_name]" + //e.g. "scott/tiger@//mymachine:1521/oracle" + $proto_opts = $dsn; + $dsn = null; + } elseif (strpos($dsn, '/') !== false) { + list($proto_opts, $dsn) = explode('/', $dsn, 2); + } else { + $proto_opts = $dsn; + $dsn = null; + } + } + + // process the different protocol options + $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp'; + $proto_opts = rawurldecode($proto_opts); + if (strpos($proto_opts, ':') !== false) { + list($proto_opts, $parsed['port']) = explode(':', $proto_opts); + } + if ($parsed['protocol'] == 'tcp') { + $parsed['hostspec'] = $proto_opts; + } elseif ($parsed['protocol'] == 'unix') { + $parsed['socket'] = $proto_opts; + } + + // Get dabase if any + // $dsn => database + if ($dsn) { + // /database + if (($pos = strpos($dsn, '?')) === false) { + $parsed['database'] = $dsn; + // /database?param1=value1¶m2=value2 + } else { + $parsed['database'] = substr($dsn, 0, $pos); + $dsn = substr($dsn, $pos + 1); + if (strpos($dsn, '&') !== false) { + $opts = explode('&', $dsn); + } else { // database?param1=value1 + $opts = array($dsn); + } + foreach ($opts as $opt) { + list($key, $value) = explode('=', $opt); + if (!isset($parsed[$key])) { + // don't allow params overwrite + $parsed[$key] = rawurldecode($value); + } + } + } + } + + return $parsed; + } + + // }}} + // {{{ function fileExists($file) + + /** + * Checks if a file exists in the include path + * + * @param string filename + * + * @return bool true success and false on error + * + * @access public + */ + function fileExists($file) + { + // safe_mode does notwork with is_readable() + if (!@ini_get('safe_mode')) { + $dirs = explode(PATH_SEPARATOR, ini_get('include_path')); + foreach ($dirs as $dir) { + if (is_readable($dir . DIRECTORY_SEPARATOR . $file)) { + return true; + } + } + } else { + $fp = @fopen($file, 'r', true); + if (is_resource($fp)) { + @fclose($fp); + return true; + } + } + return false; + } + // }}} +} + +// }}} +// {{{ class MDB2_Error extends PEAR_Error + +/** + * MDB2_Error implements a class for reporting portable database error + * messages. + * + * @package MDB2 + * @category Database + * @author Stig Bakken + */ +class MDB2_Error extends PEAR_Error +{ + // {{{ constructor: function MDB2_Error($code = MDB2_ERROR, $mode = PEAR_ERROR_RETURN, $level = E_USER_NOTICE, $debuginfo = null) + + /** + * MDB2_Error constructor. + * + * @param mixed MDB2 error code, or string with error message. + * @param int what 'error mode' to operate in + * @param int what error level to use for $mode & PEAR_ERROR_TRIGGER + * @param smixed additional debug info, such as the last query + */ + function MDB2_Error($code = MDB2_ERROR, $mode = PEAR_ERROR_RETURN, + $level = E_USER_NOTICE, $debuginfo = null) + { + if (is_null($code)) { + $code = MDB2_ERROR; + } + $this->PEAR_Error('MDB2 Error: '.MDB2::errorMessage($code), $code, + $mode, $level, $debuginfo); + } + + // }}} +} + +// }}} +// {{{ class MDB2_Driver_Common extends PEAR + +/** + * MDB2_Driver_Common: Base class that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Common extends PEAR +{ + // {{{ Variables (Properties) + + /** + * index of the MDB2 object within the $GLOBALS['_MDB2_databases'] array + * @var int + * @access public + */ + var $db_index = 0; + + /** + * DSN used for the next query + * @var array + * @access protected + */ + var $dsn = array(); + + /** + * DSN that was used to create the current connection + * @var array + * @access protected + */ + var $connected_dsn = array(); + + /** + * connection resource + * @var mixed + * @access protected + */ + var $connection = 0; + + /** + * if the current opened connection is a persistent connection + * @var bool + * @access protected + */ + var $opened_persistent; + + /** + * the name of the database for the next query + * @var string + * @access protected + */ + var $database_name = ''; + + /** + * the name of the database currently selected + * @var string + * @access protected + */ + var $connected_database_name = ''; + + /** + * server version information + * @var string + * @access protected + */ + var $connected_server_info = ''; + + /** + * list of all supported features of the given driver + * @var array + * @access public + */ + var $supported = array( + 'sequences' => false, + 'indexes' => false, + 'affected_rows' => false, + 'summary_functions' => false, + 'order_by_text' => false, + 'transactions' => false, + 'savepoints' => false, + 'current_id' => false, + 'limit_queries' => false, + 'LOBs' => false, + 'replace' => false, + 'sub_selects' => false, + 'auto_increment' => false, + 'primary_key' => false, + 'result_introspection' => false, + 'prepared_statements' => false, + 'identifier_quoting' => false, + 'pattern_escaping' => false, + 'new_link' => false, + ); + + /** + * Array of supported options that can be passed to the MDB2 instance. + * + * The options can be set during object creation, using + * MDB2::connect(), MDB2::factory() or MDB2::singleton(). The options can + * also be set after the object is created, using MDB2::setOptions() or + * MDB2_Driver_Common::setOption(). + * The list of available option includes: + *
    + *
  • $options['ssl'] -> boolean: determines if ssl should be used for connections
  • + *
  • $options['field_case'] -> CASE_LOWER|CASE_UPPER: determines what case to force on field/table names
  • + *
  • $options['disable_query'] -> boolean: determines if queries should be executed
  • + *
  • $options['result_class'] -> string: class used for result sets
  • + *
  • $options['buffered_result_class'] -> string: class used for buffered result sets
  • + *
  • $options['result_wrap_class'] -> string: class used to wrap result sets into
  • + *
  • $options['result_buffering'] -> boolean should results be buffered or not?
  • + *
  • $options['fetch_class'] -> string: class to use when fetch mode object is used
  • + *
  • $options['persistent'] -> boolean: persistent connection?
  • + *
  • $options['debug'] -> integer: numeric debug level
  • + *
  • $options['debug_handler'] -> string: function/method that captures debug messages
  • + *
  • $options['debug_expanded_output'] -> bool: BC option to determine if more context information should be send to the debug handler
  • + *
  • $options['default_text_field_length'] -> integer: default text field length to use
  • + *
  • $options['lob_buffer_length'] -> integer: LOB buffer length
  • + *
  • $options['log_line_break'] -> string: line-break format
  • + *
  • $options['idxname_format'] -> string: pattern for index name
  • + *
  • $options['seqname_format'] -> string: pattern for sequence name
  • + *
  • $options['savepoint_format'] -> string: pattern for auto generated savepoint names
  • + *
  • $options['statement_format'] -> string: pattern for prepared statement names
  • + *
  • $options['seqcol_name'] -> string: sequence column name
  • + *
  • $options['quote_identifier'] -> boolean: if identifier quoting should be done when check_option is used
  • + *
  • $options['use_transactions'] -> boolean: if transaction use should be enabled
  • + *
  • $options['decimal_places'] -> integer: number of decimal places to handle
  • + *
  • $options['portability'] -> integer: portability constant
  • + *
  • $options['modules'] -> array: short to long module name mapping for __call()
  • + *
  • $options['emulate_prepared'] -> boolean: force prepared statements to be emulated
  • + *
  • $options['datatype_map'] -> array: map user defined datatypes to other primitive datatypes
  • + *
  • $options['datatype_map_callback'] -> array: callback function/method that should be called
  • + *
+ * + * @var array + * @access public + * @see MDB2::connect() + * @see MDB2::factory() + * @see MDB2::singleton() + * @see MDB2_Driver_Common::setOption() + */ + var $options = array( + 'ssl' => false, + 'field_case' => CASE_LOWER, + 'disable_query' => false, + 'result_class' => 'MDB2_Result_%s', + 'buffered_result_class' => 'MDB2_BufferedResult_%s', + 'result_wrap_class' => false, + 'result_buffering' => true, + 'fetch_class' => 'stdClass', + 'persistent' => false, + 'debug' => 0, + 'debug_handler' => 'MDB2_defaultDebugOutput', + 'debug_expanded_output' => false, + 'default_text_field_length' => 4096, + 'lob_buffer_length' => 8192, + 'log_line_break' => "\n", + 'idxname_format' => '%s_idx', + 'seqname_format' => '%s_seq', + 'savepoint_format' => 'MDB2_SAVEPOINT_%s', + 'statement_format' => 'MDB2_STATEMENT_%1$s_%2$s', + 'seqcol_name' => 'sequence', + 'quote_identifier' => false, + 'use_transactions' => true, + 'decimal_places' => 2, + 'portability' => MDB2_PORTABILITY_ALL, + 'modules' => array( + 'ex' => 'Extended', + 'dt' => 'Datatype', + 'mg' => 'Manager', + 'rv' => 'Reverse', + 'na' => 'Native', + 'fc' => 'Function', + ), + 'emulate_prepared' => false, + 'datatype_map' => array(), + 'datatype_map_callback' => array(), + 'nativetype_map_callback' => array(), + ); + + /** + * string array + * @var string + * @access protected + */ + var $string_quoting = array('start' => "'", 'end' => "'", 'escape' => false, 'escape_pattern' => false); + + /** + * identifier quoting + * @var array + * @access protected + */ + var $identifier_quoting = array('start' => '"', 'end' => '"', 'escape' => '"'); + + /** + * sql comments + * @var array + * @access protected + */ + var $sql_comments = array( + array('start' => '--', 'end' => "\n", 'escape' => false), + array('start' => '/*', 'end' => '*/', 'escape' => false), + ); + + /** + * comparision wildcards + * @var array + * @access protected + */ + var $wildcards = array('%', '_'); + + /** + * column alias keyword + * @var string + * @access protected + */ + var $as_keyword = ' AS '; + + /** + * warnings + * @var array + * @access protected + */ + var $warnings = array(); + + /** + * string with the debugging information + * @var string + * @access public + */ + var $debug_output = ''; + + /** + * determine if there is an open transaction + * @var bool + * @access protected + */ + var $in_transaction = false; + + /** + * the smart transaction nesting depth + * @var int + * @access protected + */ + var $nested_transaction_counter = null; + + /** + * the first error that occured inside a nested transaction + * @var MDB2_Error|bool + * @access protected + */ + var $has_transaction_error = false; + + /** + * result offset used in the next query + * @var int + * @access protected + */ + var $offset = 0; + + /** + * result limit used in the next query + * @var int + * @access protected + */ + var $limit = 0; + + /** + * Database backend used in PHP (mysql, odbc etc.) + * @var string + * @access public + */ + var $phptype; + + /** + * Database used with regards to SQL syntax etc. + * @var string + * @access public + */ + var $dbsyntax; + + /** + * the last query sent to the driver + * @var string + * @access public + */ + var $last_query; + + /** + * the default fetchmode used + * @var int + * @access protected + */ + var $fetchmode = MDB2_FETCHMODE_ORDERED; + + /** + * array of module instances + * @var array + * @access protected + */ + var $modules = array(); + + /** + * determines of the PHP4 destructor emulation has been enabled yet + * @var array + * @access protected + */ + var $destructor_registered = true; + + // }}} + // {{{ constructor: function __construct() + + /** + * Constructor + */ + function __construct() + { + end($GLOBALS['_MDB2_databases']); + $db_index = key($GLOBALS['_MDB2_databases']) + 1; + $GLOBALS['_MDB2_databases'][$db_index] = &$this; + $this->db_index = $db_index; + } + + // }}} + // {{{ function MDB2_Driver_Common() + + /** + * PHP 4 Constructor + */ + function MDB2_Driver_Common() + { + $this->destructor_registered = false; + $this->__construct(); + } + + // }}} + // {{{ destructor: function __destruct() + + /** + * Destructor + */ + function __destruct() + { + $this->disconnect(false); + } + + // }}} + // {{{ function free() + + /** + * Free the internal references so that the instance can be destroyed + * + * @return bool true on success, false if result is invalid + * + * @access public + */ + function free() + { + unset($GLOBALS['_MDB2_databases'][$this->db_index]); + unset($this->db_index); + return MDB2_OK; + } + + // }}} + // {{{ function __toString() + + /** + * String conversation + * + * @return string representation of the object + * + * @access public + */ + function __toString() + { + $info = get_class($this); + $info.= ': (phptype = '.$this->phptype.', dbsyntax = '.$this->dbsyntax.')'; + if ($this->connection) { + $info.= ' [connected]'; + } + return $info; + } + + // }}} + // {{{ function errorInfo($error = null) + + /** + * This method is used to collect information about an error + * + * @param mixed error code or resource + * + * @return array with MDB2 errorcode, native error code, native message + * + * @access public + */ + function errorInfo($error = null) + { + return array($error, null, null); + } + + // }}} + // {{{ function &raiseError($code = null, $mode = null, $options = null, $userinfo = null) + + /** + * This method is used to communicate an error and invoke error + * callbacks etc. Basically a wrapper for PEAR::raiseError + * without the message string. + * + * @param mixed integer error code, or a PEAR error object (all other + * parameters are ignored if this parameter is an object + * @param int error mode, see PEAR_Error docs + * @param mixed If error mode is PEAR_ERROR_TRIGGER, this is the + * error level (E_USER_NOTICE etc). If error mode is + * PEAR_ERROR_CALLBACK, this is the callback function, + * either as a function name, or as an array of an + * object and method name. For other error modes this + * parameter is ignored. + * @param string Extra debug information. Defaults to the last + * query and native error code. + * @param string name of the method that triggered the error + * + * @return PEAR_Error instance of a PEAR Error object + * + * @access public + * @see PEAR_Error + */ + function &raiseError($code = null, $mode = null, $options = null, $userinfo = null, $method = null) + { + $userinfo = "[Error message: $userinfo]\n"; + // The error is yet a MDB2 error object + if (PEAR::isError($code)) { + // because we use the static PEAR::raiseError, our global + // handler should be used if it is set + if (is_null($mode) && !empty($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + } + if (is_null($userinfo)) { + $userinfo = $code->getUserinfo(); + } + $code = $code->getCode(); + } elseif ($code == MDB2_ERROR_NOT_FOUND) { + // extension not loaded: don't call $this->errorInfo() or the script + // will die + } elseif (isset($this->connection)) { + if (!empty($this->last_query)) { + $userinfo.= "[Last executed query: {$this->last_query}]\n"; + } + $native_errno = $native_msg = null; + list($code, $native_errno, $native_msg) = $this->errorInfo($code); + if (!is_null($native_errno) && $native_errno !== '') { + $userinfo.= "[Native code: $native_errno]\n"; + } + if (!is_null($native_msg) && $native_msg !== '') { + $userinfo.= "[Native message: ". strip_tags($native_msg) ."]\n"; + } + if (!is_null($method)) { + $userinfo = $method.': '.$userinfo; + } + } + + $err =& PEAR::raiseError(null, $code, $mode, $options, $userinfo, 'MDB2_Error', true); + if ($err->getMode() !== PEAR_ERROR_RETURN + && isset($this->nested_transaction_counter) && !$this->has_transaction_error) { + $this->has_transaction_error =& $err; + } + return $err; + } + + // }}} + // {{{ function resetWarnings() + + /** + * reset the warning array + * + * @return void + * + * @access public + */ + function resetWarnings() + { + $this->warnings = array(); + } + + // }}} + // {{{ function getWarnings() + + /** + * Get all warnings in reverse order. + * This means that the last warning is the first element in the array + * + * @return array with warnings + * + * @access public + * @see resetWarnings() + */ + function getWarnings() + { + return array_reverse($this->warnings); + } + + // }}} + // {{{ function setFetchMode($fetchmode, $object_class = 'stdClass') + + /** + * Sets which fetch mode should be used by default on queries + * on this connection + * + * @param int MDB2_FETCHMODE_ORDERED, MDB2_FETCHMODE_ASSOC + * or MDB2_FETCHMODE_OBJECT + * @param string the class name of the object to be returned + * by the fetch methods when the + * MDB2_FETCHMODE_OBJECT mode is selected. + * If no class is specified by default a cast + * to object from the assoc array row will be + * done. There is also the possibility to use + * and extend the 'MDB2_row' class. + * + * @return mixed MDB2_OK or MDB2 Error Object + * + * @access public + * @see MDB2_FETCHMODE_ORDERED, MDB2_FETCHMODE_ASSOC, MDB2_FETCHMODE_OBJECT + */ + function setFetchMode($fetchmode, $object_class = 'stdClass') + { + switch ($fetchmode) { + case MDB2_FETCHMODE_OBJECT: + $this->options['fetch_class'] = $object_class; + case MDB2_FETCHMODE_ORDERED: + case MDB2_FETCHMODE_ASSOC: + $this->fetchmode = $fetchmode; + break; + default: + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'invalid fetchmode mode', __FUNCTION__); + } + + return MDB2_OK; + } + + // }}} + // {{{ function setOption($option, $value) + + /** + * set the option for the db class + * + * @param string option name + * @param mixed value for the option + * + * @return mixed MDB2_OK or MDB2 Error Object + * + * @access public + */ + function setOption($option, $value) + { + if (array_key_exists($option, $this->options)) { + $this->options[$option] = $value; + return MDB2_OK; + } + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + "unknown option $option", __FUNCTION__); + } + + // }}} + // {{{ function getOption($option) + + /** + * Returns the value of an option + * + * @param string option name + * + * @return mixed the option value or error object + * + * @access public + */ + function getOption($option) + { + if (array_key_exists($option, $this->options)) { + return $this->options[$option]; + } + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + "unknown option $option", __FUNCTION__); + } + + // }}} + // {{{ function debug($message, $scope = '', $is_manip = null) + + /** + * set a debug message + * + * @param string message that should be appended to the debug variable + * @param string usually the method name that triggered the debug call: + * for example 'query', 'prepare', 'execute', 'parameters', + * 'beginTransaction', 'commit', 'rollback' + * @param array contains context information about the debug() call + * common keys are: is_manip, time, result etc. + * + * @return void + * + * @access public + */ + function debug($message, $scope = '', $context = array()) + { + if ($this->options['debug'] && $this->options['debug_handler']) { + if (!$this->options['debug_expanded_output']) { + if (!empty($context['when']) && $context['when'] !== 'pre') { + return null; + } + $context = empty($context['is_manip']) ? false : $context['is_manip']; + } + return call_user_func_array($this->options['debug_handler'], array(&$this, $scope, $message, $context)); + } + return null; + } + + // }}} + // {{{ function getDebugOutput() + + /** + * output debug info + * + * @return string content of the debug_output class variable + * + * @access public + */ + function getDebugOutput() + { + return $this->debug_output; + } + + // }}} + // {{{ function escape($text) + + /** + * Quotes a string so it can be safely used in a query. It will quote + * the text so it can safely be used within a query. + * + * @param string the input string to quote + * @param bool escape wildcards + * + * @return string quoted string + * + * @access public + */ + function escape($text, $escape_wildcards = false) + { + if ($escape_wildcards) { + $text = $this->escapePattern($text); + } + + $text = str_replace($this->string_quoting['end'], $this->string_quoting['escape'] . $this->string_quoting['end'], $text); + return $text; + } + + // }}} + // {{{ function escapePattern($text) + + /** + * Quotes pattern (% and _) characters in a string) + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @param string the input string to quote + * + * @return string quoted string + * + * @access public + */ + function escapePattern($text) + { + if ($this->string_quoting['escape_pattern']) { + $text = str_replace($this->string_quoting['escape_pattern'], $this->string_quoting['escape_pattern'] . $this->string_quoting['escape_pattern'], $text); + foreach ($this->wildcards as $wildcard) { + $text = str_replace($wildcard, $this->string_quoting['escape_pattern'] . $wildcard, $text); + } + } + return $text; + } + + // }}} + // {{{ function quoteIdentifier($str, $check_option = false) + + /** + * Quote a string so it can be safely used as a table or column name + * + * Delimiting style depends on which database driver is being used. + * + * NOTE: just because you CAN use delimited identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * Portability is broken by using the following characters inside + * delimited identifiers: + * + backtick (`) -- due to MySQL + * + double quote (") -- due to Oracle + * + brackets ([ or ]) -- due to Access + * + * Delimited identifiers are known to generally work correctly under + * the following drivers: + * + mssql + * + mysql + * + mysqli + * + oci8 + * + pgsql + * + sqlite + * + * InterBase doesn't seem to be able to use delimited identifiers + * via PHP 4. They work fine under PHP 5. + * + * @param string identifier name to be quoted + * @param bool check the 'quote_identifier' option + * + * @return string quoted identifier string + * + * @access public + */ + function quoteIdentifier($str, $check_option = false) + { + if ($check_option && !$this->options['quote_identifier']) { + return $str; + } + $str = str_replace($this->identifier_quoting['end'], $this->identifier_quoting['escape'] . $this->identifier_quoting['end'], $str); + return $this->identifier_quoting['start'] . $str . $this->identifier_quoting['end']; + } + + // }}} + // {{{ function getAsKeyword() + + /** + * Gets the string to alias column + * + * @return string to use when aliasing a column + */ + function getAsKeyword() + { + return $this->as_keyword; + } + + // }}} + // {{{ function getConnection() + + /** + * Returns a native connection + * + * @return mixed a valid MDB2 connection object, + * or a MDB2 error object on error + * + * @access public + */ + function getConnection() + { + $result = $this->connect(); + if (PEAR::isError($result)) { + return $result; + } + return $this->connection; + } + + // }}} + // {{{ function _fixResultArrayValues(&$row, $mode) + + /** + * Do all necessary conversions on result arrays to fix DBMS quirks + * + * @param array the array to be fixed (passed by reference) + * @param array bit-wise addition of the required portability modes + * + * @return void + * + * @access protected + */ + function _fixResultArrayValues(&$row, $mode) + { + switch ($mode) { + case MDB2_PORTABILITY_EMPTY_TO_NULL: + foreach ($row as $key => $value) { + if ($value === '') { + $row[$key] = null; + } + } + break; + case MDB2_PORTABILITY_RTRIM: + foreach ($row as $key => $value) { + if (is_string($value)) { + $row[$key] = rtrim($value); + } + } + break; + case MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES: + $tmp_row = array(); + foreach ($row as $key => $value) { + $tmp_row[preg_replace('/^(?:.*\.)?([^.]+)$/', '\\1', $key)] = $value; + } + $row = $tmp_row; + break; + case (MDB2_PORTABILITY_RTRIM + MDB2_PORTABILITY_EMPTY_TO_NULL): + foreach ($row as $key => $value) { + if ($value === '') { + $row[$key] = null; + } elseif (is_string($value)) { + $row[$key] = rtrim($value); + } + } + break; + case (MDB2_PORTABILITY_RTRIM + MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES): + $tmp_row = array(); + foreach ($row as $key => $value) { + if (is_string($value)) { + $value = rtrim($value); + } + $tmp_row[preg_replace('/^(?:.*\.)?([^.]+)$/', '\\1', $key)] = $value; + } + $row = $tmp_row; + break; + case (MDB2_PORTABILITY_EMPTY_TO_NULL + MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES): + $tmp_row = array(); + foreach ($row as $key => $value) { + if ($value === '') { + $value = null; + } + $tmp_row[preg_replace('/^(?:.*\.)?([^.]+)$/', '\\1', $key)] = $value; + } + $row = $tmp_row; + break; + case (MDB2_PORTABILITY_RTRIM + MDB2_PORTABILITY_EMPTY_TO_NULL + MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES): + $tmp_row = array(); + foreach ($row as $key => $value) { + if ($value === '') { + $value = null; + } elseif (is_string($value)) { + $value = rtrim($value); + } + $tmp_row[preg_replace('/^(?:.*\.)?([^.]+)$/', '\\1', $key)] = $value; + } + $row = $tmp_row; + break; + } + } + + // }}} + // {{{ function &loadModule($module, $property = null, $phptype_specific = null) + + /** + * loads a module + * + * @param string name of the module that should be loaded + * (only used for error messages) + * @param string name of the property into which the class will be loaded + * @param bool if the class to load for the module is specific to the + * phptype + * + * @return object on success a reference to the given module is returned + * and on failure a PEAR error + * + * @access public + */ + function &loadModule($module, $property = null, $phptype_specific = null) + { + if (!$property) { + $property = strtolower($module); + } + + if (!isset($this->{$property})) { + $version = $phptype_specific; + if ($phptype_specific !== false) { + $version = true; + $class_name = 'MDB2_Driver_'.$module.'_'.$this->phptype; + $file_name = str_replace('_', DIRECTORY_SEPARATOR, $class_name).'.php'; + } + if ($phptype_specific === false + || (!MDB2::classExists($class_name) && !MDB2::fileExists($file_name)) + ) { + $version = false; + $class_name = 'MDB2_'.$module; + $file_name = str_replace('_', DIRECTORY_SEPARATOR, $class_name).'.php'; + } + + $err = MDB2::loadClass($class_name, $this->getOption('debug')); + if (PEAR::isError($err)) { + return $err; + } + + // load modul in a specific version + if ($version) { + if (method_exists($class_name, 'getClassName')) { + $class_name_new = call_user_func(array($class_name, 'getClassName'), $this->db_index); + if ($class_name != $class_name_new) { + $class_name = $class_name_new; + $err = MDB2::loadClass($class_name, $this->getOption('debug')); + if (PEAR::isError($err)) { + return $err; + } + } + } + } + + if (!MDB2::classExists($class_name)) { + $err =& $this->raiseError(MDB2_ERROR_LOADMODULE, null, null, + "unable to load module '$module' into property '$property'", __FUNCTION__); + return $err; + } + $this->{$property} =& new $class_name($this->db_index); + $this->modules[$module] =& $this->{$property}; + if ($version) { + // this will be used in the connect method to determine if the module + // needs to be loaded with a different version if the server + // version changed in between connects + $this->loaded_version_modules[] = $property; + } + } + + return $this->{$property}; + } + + // }}} + // {{{ function __call($method, $params) + + /** + * Calls a module method using the __call magic method + * + * @param string Method name. + * @param array Arguments. + * + * @return mixed Returned value. + */ + function __call($method, $params) + { + $module = null; + if (preg_match('/^([a-z]+)([A-Z])(.*)$/', $method, $match) + && isset($this->options['modules'][$match[1]]) + ) { + $module = $this->options['modules'][$match[1]]; + $method = strtolower($match[2]).$match[3]; + if (!isset($this->modules[$module]) || !is_object($this->modules[$module])) { + $result =& $this->loadModule($module); + if (PEAR::isError($result)) { + return $result; + } + } + } else { + foreach ($this->modules as $key => $foo) { + if (is_object($this->modules[$key]) + && method_exists($this->modules[$key], $method) + ) { + $module = $key; + break; + } + } + } + if (!is_null($module)) { + return call_user_func_array(array(&$this->modules[$module], $method), $params); + } + trigger_error(sprintf('Call to undefined function: %s::%s().', get_class($this), $method), E_USER_ERROR); + } + + // }}} + // {{{ function beginTransaction($savepoint = null) + + /** + * Start a transaction or set a savepoint. + * + * @param string name of a savepoint to set + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function beginTransaction($savepoint = null) + { + $this->debug('Starting transaction', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'transactions are not supported', __FUNCTION__); + } + + // }}} + // {{{ function commit($savepoint = null) + + /** + * Commit the database changes done during a transaction that is in + * progress or release a savepoint. This function may only be called when + * auto-committing is disabled, otherwise it will fail. Therefore, a new + * transaction is implicitly started after committing the pending changes. + * + * @param string name of a savepoint to release + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function commit($savepoint = null) + { + $this->debug('Committing transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'commiting transactions is not supported', __FUNCTION__); + } + + // }}} + // {{{ function rollback($savepoint = null) + + /** + * Cancel any database changes done during a transaction or since a specific + * savepoint that is in progress. This function may only be called when + * auto-committing is disabled, otherwise it will fail. Therefore, a new + * transaction is implicitly started after canceling the pending changes. + * + * @param string name of a savepoint to rollback to + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function rollback($savepoint = null) + { + $this->debug('Rolling back transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'rolling back transactions is not supported', __FUNCTION__); + } + + // }}} + // {{{ function inTransaction($ignore_nested = false) + + /** + * If a transaction is currently open. + * + * @param bool if the nested transaction count should be ignored + * @return int|bool - an integer with the nesting depth is returned if a + * nested transaction is open + * - true is returned for a normal open transaction + * - false is returned if no transaction is open + * + * @access public + */ + function inTransaction($ignore_nested = false) + { + if (!$ignore_nested && isset($this->nested_transaction_counter)) { + return $this->nested_transaction_counter; + } + return $this->in_transaction; + } + + // }}} + // {{{ function setTransactionIsolation($isolation) + + /** + * Set the transacton isolation level. + * + * @param string standard isolation level + * READ UNCOMMITTED (allows dirty reads) + * READ COMMITTED (prevents dirty reads) + * REPEATABLE READ (prevents nonrepeatable reads) + * SERIALIZABLE (prevents phantom reads) + * @param array some transaction options: + * 'wait' => 'WAIT' | 'NO WAIT' + * 'rw' => 'READ WRITE' | 'READ ONLY' + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + * @since 2.1.1 + */ + function setTransactionIsolation($isolation, $options = array()) + { + $this->debug('Setting transaction isolation level', __FUNCTION__, array('is_manip' => true)); + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'isolation level setting is not supported', __FUNCTION__); + } + + // }}} + // {{{ function beginNestedTransaction($savepoint = false) + + /** + * Start a nested transaction. + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @return mixed MDB2_OK on success/savepoint name, a MDB2 error on failure + * + * @access public + * @since 2.1.1 + */ + function beginNestedTransaction() + { + if ($this->in_transaction) { + ++$this->nested_transaction_counter; + $savepoint = sprintf($this->options['savepoint_format'], $this->nested_transaction_counter); + if ($this->supports('savepoints') && $savepoint) { + return $this->beginTransaction($savepoint); + } + return MDB2_OK; + } + $this->has_transaction_error = false; + $result = $this->beginTransaction(); + $this->nested_transaction_counter = 1; + return $result; + } + + // }}} + // {{{ function completeNestedTransaction($force_rollback = false, $release = false) + + /** + * Finish a nested transaction by rolling back if an error occured or + * committing otherwise. + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @param bool if the transaction should be rolled back regardless + * even if no error was set within the nested transaction + * @return mixed MDB_OK on commit/counter decrementing, false on rollback + * and a MDB2 error on failure + * + * @access public + * @since 2.1.1 + */ + function completeNestedTransaction($force_rollback = false) + { + if ($this->nested_transaction_counter > 1) { + $savepoint = sprintf($this->options['savepoint_format'], $this->nested_transaction_counter); + if ($this->supports('savepoints') && $savepoint) { + if ($force_rollback || $this->has_transaction_error) { + $result = $this->rollback($savepoint); + if (!PEAR::isError($result)) { + $result = false; + $this->has_transaction_error = false; + } + } else { + $result = $this->commit($savepoint); + } + } else { + $result = MDB2_OK; + } + --$this->nested_transaction_counter; + return $result; + } + + $this->nested_transaction_counter = null; + $result = MDB2_OK; + + // transaction has not yet been rolled back + if ($this->in_transaction) { + if ($force_rollback || $this->has_transaction_error) { + $result = $this->rollback(); + if (!PEAR::isError($result)) { + $result = false; + } + } else { + $result = $this->commit(); + } + } + $this->has_transaction_error = false; + return $result; + } + + // }}} + // {{{ function failNestedTransaction($error = null, $immediately = false) + + /** + * Force setting nested transaction to failed. + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @param mixed value to return in getNestededTransactionError() + * @param bool if the transaction should be rolled back immediately + * @return bool MDB2_OK + * + * @access public + * @since 2.1.1 + */ + function failNestedTransaction($error = null, $immediately = false) + { + if (is_null($error)) { + $error = $this->has_transaction_error ? $this->has_transaction_error : true; + } elseif (!$error) { + $error = true; + } + $this->has_transaction_error = $error; + if (!$immediately) { + return MDB2_OK; + } + return $this->rollback(); + } + + // }}} + // {{{ function getNestedTransactionError() + + /** + * The first error that occured since the transaction start. + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @return MDB2_Error|bool MDB2 error object if an error occured or false. + * + * @access public + * @since 2.1.1 + */ + function getNestedTransactionError() + { + return $this->has_transaction_error; + } + + // }}} + // {{{ connect() + + /** + * Connect to the database + * + * @return true on success, MDB2 Error Object on failure + */ + function connect() + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ setCharset($charset, $connection = null) + + /** + * Set the charset on the current connection + * + * @param string charset + * @param resource connection handle + * + * @return true on success, MDB2 Error Object on failure + */ + function setCharset($charset, $connection = null) + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function disconnect($force = true) + + /** + * Log out and disconnect from the database. + * + * @param bool if the disconnect should be forced even if the + * connection is opened persistently + * + * @return mixed true on success, false if not connected and error + * object on error + * + * @access public + */ + function disconnect($force = true) + { + $this->connection = 0; + $this->connected_dsn = array(); + $this->connected_database_name = ''; + $this->opened_persistent = null; + $this->connected_server_info = ''; + $this->in_transaction = null; + $this->nested_transaction_counter = null; + return MDB2_OK; + } + + // }}} + // {{{ function setDatabase($name) + + /** + * Select a different database + * + * @param string name of the database that should be selected + * + * @return string name of the database previously connected to + * + * @access public + */ + function setDatabase($name) + { + $previous_database_name = (isset($this->database_name)) ? $this->database_name : ''; + $this->database_name = $name; + $this->disconnect(false); + return $previous_database_name; + } + + // }}} + // {{{ function getDatabase() + + /** + * Get the current database + * + * @return string name of the database + * + * @access public + */ + function getDatabase() + { + return $this->database_name; + } + + // }}} + // {{{ function setDSN($dsn) + + /** + * set the DSN + * + * @param mixed DSN string or array + * + * @return MDB2_OK + * + * @access public + */ + function setDSN($dsn) + { + $dsn_default = $GLOBALS['_MDB2_dsninfo_default']; + $dsn = MDB2::parseDSN($dsn); + if (array_key_exists('database', $dsn)) { + $this->database_name = $dsn['database']; + unset($dsn['database']); + } + $this->dsn = array_merge($dsn_default, $dsn); + return $this->disconnect(false); + } + + // }}} + // {{{ function getDSN($type = 'string', $hidepw = false) + + /** + * return the DSN as a string + * + * @param string format to return ("array", "string") + * @param string string to hide the password with + * + * @return mixed DSN in the chosen type + * + * @access public + */ + function getDSN($type = 'string', $hidepw = false) + { + $dsn = array_merge($GLOBALS['_MDB2_dsninfo_default'], $this->dsn); + $dsn['phptype'] = $this->phptype; + $dsn['database'] = $this->database_name; + if ($hidepw) { + $dsn['password'] = $hidepw; + } + switch ($type) { + // expand to include all possible options + case 'string': + $dsn = $dsn['phptype']. + ($dsn['dbsyntax'] ? ('('.$dsn['dbsyntax'].')') : ''). + '://'.$dsn['username'].':'. + $dsn['password'].'@'.$dsn['hostspec']. + ($dsn['port'] ? (':'.$dsn['port']) : ''). + '/'.$dsn['database']; + break; + case 'array': + default: + break; + } + return $dsn; + } + + // }}} + // {{{ function &standaloneQuery($query, $types = null, $is_manip = false) + + /** + * execute a query as database administrator + * + * @param string the SQL query + * @param mixed array that contains the types of the columns in + * the result set + * @param bool if the query is a manipulation query + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function &standaloneQuery($query, $types = null, $is_manip = false) + { + $offset = $this->offset; + $limit = $this->limit; + $this->offset = $this->limit = 0; + $query = $this->_modifyQuery($query, $is_manip, $limit, $offset); + + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $result =& $this->_doQuery($query, $is_manip, $connection, false); + if (PEAR::isError($result)) { + return $result; + } + + if ($is_manip) { + $affected_rows = $this->_affectedRows($connection, $result); + return $affected_rows; + } + $result =& $this->_wrapResult($result, $types, true, false, $limit, $offset); + return $result; + } + + // }}} + // {{{ function _modifyQuery($query, $is_manip, $limit, $offset) + + /** + * Changes a query string for various DBMS specific reasons + * + * @param string query to modify + * @param bool if it is a DML query + * @param int limit the number of rows + * @param int start reading from given offset + * + * @return string modified query + * + * @access protected + */ + function _modifyQuery($query, $is_manip, $limit, $offset) + { + return $query; + } + + // }}} + // {{{ function &_doQuery($query, $is_manip = false, $connection = null, $database_name = null) + + /** + * Execute a query + * @param string query + * @param bool if the query is a manipulation query + * @param resource connection handle + * @param string database name + * + * @return result or error object + * + * @access protected + */ + function &_doQuery($query, $is_manip = false, $connection = null, $database_name = null) + { + $this->last_query = $query; + $result = $this->debug($query, 'query', array('is_manip' => $is_manip, 'when' => 'pre')); + if ($result) { + if (PEAR::isError($result)) { + return $result; + } + $query = $result; + } + $err =& $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + return $err; + } + + // }}} + // {{{ function _affectedRows($connection, $result = null) + + /** + * Returns the number of rows affected + * + * @param resource result handle + * @param resource connection handle + * + * @return mixed MDB2 Error Object or the number of rows affected + * + * @access private + */ + function _affectedRows($connection, $result = null) + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function &exec($query) + + /** + * Execute a manipulation query to the database and return the number of affected rows + * + * @param string the SQL query + * + * @return mixed number of affected rows on success, a MDB2 error on failure + * + * @access public + */ + function &exec($query) + { + $offset = $this->offset; + $limit = $this->limit; + $this->offset = $this->limit = 0; + $query = $this->_modifyQuery($query, true, $limit, $offset); + + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $result =& $this->_doQuery($query, true, $connection, $this->database_name); + if (PEAR::isError($result)) { + return $result; + } + + $affectedRows = $this->_affectedRows($connection, $result); + return $affectedRows; + } + + // }}} + // {{{ function &query($query, $types = null, $result_class = true, $result_wrap_class = false) + + /** + * Send a query to the database and return any results + * + * @param string the SQL query + * @param mixed array that contains the types of the columns in + * the result set + * @param mixed string which specifies which result class to use + * @param mixed string which specifies which class to wrap results in + * + * @return mixed an MDB2_Result handle on success, a MDB2 error on failure + * + * @access public + */ + function &query($query, $types = null, $result_class = true, $result_wrap_class = false) + { + $offset = $this->offset; + $limit = $this->limit; + $this->offset = $this->limit = 0; + $query = $this->_modifyQuery($query, false, $limit, $offset); + + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $result =& $this->_doQuery($query, false, $connection, $this->database_name); + if (PEAR::isError($result)) { + return $result; + } + + $result =& $this->_wrapResult($result, $types, $result_class, $result_wrap_class, $limit, $offset); + return $result; + } + + // }}} + // {{{ function &_wrapResult($result, $types = array(), $result_class = true, $result_wrap_class = false, $limit = null, $offset = null) + + /** + * wrap a result set into the correct class + * + * @param resource result handle + * @param mixed array that contains the types of the columns in + * the result set + * @param mixed string which specifies which result class to use + * @param mixed string which specifies which class to wrap results in + * @param string number of rows to select + * @param string first row to select + * + * @return mixed an MDB2_Result, a MDB2 error on failure + * + * @access protected + */ + function &_wrapResult($result, $types = array(), $result_class = true, + $result_wrap_class = false, $limit = null, $offset = null) + { + if ($types === true) { + if ($this->supports('result_introspection')) { + $this->loadModule('Reverse', null, true); + $tableInfo = $this->reverse->tableInfo($result); + if (PEAR::isError($tableInfo)) { + return $tableInfo; + } + $types = array(); + foreach ($tableInfo as $field) { + $types[] = $field['mdb2type']; + } + } else { + $types = null; + } + } + + if ($result_class === true) { + $result_class = $this->options['result_buffering'] + ? $this->options['buffered_result_class'] : $this->options['result_class']; + } + + if ($result_class) { + $class_name = sprintf($result_class, $this->phptype); + if (!MDB2::classExists($class_name)) { + $err =& $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'result class does not exist '.$class_name, __FUNCTION__); + return $err; + } + $result =& new $class_name($this, $result, $limit, $offset); + if (!MDB2::isResultCommon($result)) { + $err =& $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'result class is not extended from MDB2_Result_Common', __FUNCTION__); + return $err; + } + if (!empty($types)) { + $err = $result->setResultTypes($types); + if (PEAR::isError($err)) { + $result->free(); + return $err; + } + } + } + if ($result_wrap_class === true) { + $result_wrap_class = $this->options['result_wrap_class']; + } + if ($result_wrap_class) { + if (!MDB2::classExists($result_wrap_class)) { + $err =& $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'result wrap class does not exist '.$result_wrap_class, __FUNCTION__); + return $err; + } + $result =& new $result_wrap_class($result, $this->fetchmode); + } + return $result; + } + + // }}} + // {{{ function getServerVersion($native = false) + + /** + * return version information about the server + * + * @param bool determines if the raw version string should be returned + * + * @return mixed array with version information or row string + * + * @access public + */ + function getServerVersion($native = false) + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function setLimit($limit, $offset = null) + + /** + * set the range of the next query + * + * @param string number of rows to select + * @param string first row to select + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function setLimit($limit, $offset = null) + { + if (!$this->supports('limit_queries')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'limit is not supported by this driver', __FUNCTION__); + } + $limit = (int)$limit; + if ($limit < 0) { + return $this->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'it was not specified a valid selected range row limit', __FUNCTION__); + } + $this->limit = $limit; + if (!is_null($offset)) { + $offset = (int)$offset; + if ($offset < 0) { + return $this->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'it was not specified a valid first selected range row', __FUNCTION__); + } + $this->offset = $offset; + } + return MDB2_OK; + } + + // }}} + // {{{ function subSelect($query, $type = false) + + /** + * simple subselect emulation: leaves the query untouched for all RDBMS + * that support subselects + * + * @param string the SQL query for the subselect that may only + * return a column + * @param string determines type of the field + * + * @return string the query + * + * @access public + */ + function subSelect($query, $type = false) + { + if ($this->supports('sub_selects') === true) { + return $query; + } + + if (!$this->supports('sub_selects')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + $col = $this->queryCol($query, $type); + if (PEAR::isError($col)) { + return $col; + } + if (!is_array($col) || count($col) == 0) { + return 'NULL'; + } + if ($type) { + $this->loadModule('Datatype', null, true); + return $this->datatype->implodeArray($col, $type); + } + return implode(', ', $col); + } + + // }}} + // {{{ function replace($table, $fields) + + /** + * Execute a SQL REPLACE query. A REPLACE query is identical to a INSERT + * query, except that if there is already a row in the table with the same + * key field values, the REPLACE query just updates its values instead of + * inserting a new row. + * + * The REPLACE type of query does not make part of the SQL standards. Since + * practically only MySQL and SQLite implement it natively, this type of + * query isemulated through this method for other DBMS using standard types + * of queries inside a transaction to assure the atomicity of the operation. + * + * @param string name of the table on which the REPLACE query will + * be executed. + * @param array associative array that describes the fields and the + * values that will be inserted or updated in the specified table. The + * indexes of the array are the names of all the fields of the table. + * The values of the array are also associative arrays that describe + * the values and other properties of the table fields. + * + * Here follows a list of field properties that need to be specified: + * + * value + * Value to be assigned to the specified field. This value may be + * of specified in database independent type format as this + * function can perform the necessary datatype conversions. + * + * Default: this property is required unless the Null property is + * set to 1. + * + * type + * Name of the type of the field. Currently, all types MDB2 + * are supported except for clob and blob. + * + * Default: no type conversion + * + * null + * bool property that indicates that the value for this field + * should be set to null. + * + * The default value for fields missing in INSERT queries may be + * specified the definition of a table. Often, the default value + * is already null, but since the REPLACE may be emulated using + * an UPDATE query, make sure that all fields of the table are + * listed in this function argument array. + * + * Default: 0 + * + * key + * bool property that indicates that this field should be + * handled as a primary key or at least as part of the compound + * unique index of the table that will determine the row that will + * updated if it exists or inserted a new row otherwise. + * + * This function will fail if no key field is specified or if the + * value of a key field is set to null because fields that are + * part of unique index they may not be null. + * + * Default: 0 + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function replace($table, $fields) + { + if (!$this->supports('replace')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'replace query is not supported', __FUNCTION__); + } + $count = count($fields); + $condition = $values = array(); + for ($colnum = 0, reset($fields); $colnum < $count; next($fields), $colnum++) { + $name = key($fields); + if (isset($fields[$name]['null']) && $fields[$name]['null']) { + $value = 'NULL'; + } else { + $type = isset($fields[$name]['type']) ? $fields[$name]['type'] : null; + $value = $this->quote($fields[$name]['value'], $type); + } + $values[$name] = $value; + if (isset($fields[$name]['key']) && $fields[$name]['key']) { + if ($value === 'NULL') { + return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null, + 'key value '.$name.' may not be NULL', __FUNCTION__); + } + $condition[] = $name . '=' . $value; + } + } + if (empty($condition)) { + return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null, + 'not specified which fields are keys', __FUNCTION__); + } + + $result = null; + $in_transaction = $this->in_transaction; + if (!$in_transaction && PEAR::isError($result = $this->beginTransaction())) { + return $result; + } + + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $condition = ' WHERE '.implode(' AND ', $condition); + $query = "DELETE FROM $table$condition"; + $result =& $this->_doQuery($query, true, $connection); + if (!PEAR::isError($result)) { + $affected_rows = $this->_affectedRows($connection, $result); + $insert = implode(', ', array_keys($values)); + $values = implode(', ', $values); + $query = "INSERT INTO $table ($insert) VALUES ($values)"; + $result =& $this->_doQuery($query, true, $connection); + if (!PEAR::isError($result)) { + $affected_rows += $this->_affectedRows($connection, $result);; + } + } + + if (!$in_transaction) { + if (PEAR::isError($result)) { + $this->rollback(); + } else { + $result = $this->commit(); + } + } + + if (PEAR::isError($result)) { + return $result; + } + + return $affected_rows; + } + + // }}} + // {{{ function &prepare($query, $types = null, $result_types = null, $lobs = array()) + + /** + * Prepares a query for multiple execution with execute(). + * With some database backends, this is emulated. + * prepare() requires a generic query as string like + * 'INSERT INTO numbers VALUES(?,?)' or + * 'INSERT INTO numbers VALUES(:foo,:bar)'. + * The ? and :[a-zA-Z] and are placeholders which can be set using + * bindParam() and the query can be send off using the execute() method. + * + * @param string the query to prepare + * @param mixed array that contains the types of the placeholders + * @param mixed array that contains the types of the columns in + * the result set or MDB2_PREPARE_RESULT, if set to + * MDB2_PREPARE_MANIP the query is handled as a manipulation query + * @param mixed key (field) value (parameter) pair for all lob placeholders + * + * @return mixed resource handle for the prepared query on success, + * a MDB2 error on failure + * + * @access public + * @see bindParam, execute + */ + function &prepare($query, $types = null, $result_types = null, $lobs = array()) + { + $is_manip = ($result_types === MDB2_PREPARE_MANIP); + $offset = $this->offset; + $limit = $this->limit; + $this->offset = $this->limit = 0; + $result = $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'pre')); + if ($result) { + if (PEAR::isError($result)) { + return $result; + } + $query = $result; + } + $placeholder_type_guess = $placeholder_type = null; + $question = '?'; + $colon = ':'; + $positions = array(); + $position = 0; + $ignores = $this->sql_comments; + $ignores[] = $this->string_quoting; + $ignores[] = $this->identifier_quoting; + while ($position < strlen($query)) { + $q_position = strpos($query, $question, $position); + $c_position = strpos($query, $colon, $position); + if ($q_position && $c_position) { + $p_position = min($q_position, $c_position); + } elseif ($q_position) { + $p_position = $q_position; + } elseif ($c_position) { + $p_position = $c_position; + } else { + break; + } + if (is_null($placeholder_type)) { + $placeholder_type_guess = $query[$p_position]; + } + + $new_pos = $this->_skipDelimitedStrings($query, $position, $p_position); + if (PEAR::isError($new_pos)) { + return $new_pos; + } + if ($new_pos != $position) { + $position = $new_pos; + continue; //evaluate again starting from the new position + } + + if ($query[$position] == $placeholder_type_guess) { + if (is_null($placeholder_type)) { + $placeholder_type = $query[$p_position]; + $question = $colon = $placeholder_type; + if (!empty($types) && is_array($types)) { + if ($placeholder_type == ':') { + if (is_int(key($types))) { + $types_tmp = $types; + $types = array(); + $count = -1; + } + } else { + $types = array_values($types); + } + } + } + if ($placeholder_type == ':') { + $parameter = preg_replace('/^.{'.($position+1).'}([a-z0-9_]+).*$/si', '\\1', $query); + if ($parameter === '') { + $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'named parameter with an empty name', __FUNCTION__); + return $err; + } + $positions[$p_position] = $parameter; + $query = substr_replace($query, '?', $position, strlen($parameter)+1); + // use parameter name in type array + if (isset($count) && isset($types_tmp[++$count])) { + $types[$parameter] = $types_tmp[$count]; + } + } else { + $positions[$p_position] = count($positions); + } + $position = $p_position + 1; + } else { + $position = $p_position; + } + } + $class_name = 'MDB2_Statement_'.$this->phptype; + $statement = null; + $obj =& new $class_name($this, $statement, $positions, $query, $types, $result_types, $is_manip, $limit, $offset); + $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'post', 'result' => $obj)); + return $obj; + } + + // }}} + // {{{ function _skipDelimitedStrings($query, $position, $p_position) + + /** + * Utility method, used by prepare() to avoid replacing placeholders within delimited strings. + * Check if the placeholder is contained within a delimited string. + * If so, skip it and advance the position, otherwise return the current position, + * which is valid + * + * @param string $query + * @param integer $position current string cursor position + * @param integer $p_position placeholder position + * + * @return mixed integer $new_position on success + * MDB2_Error on failure + * + * @access protected + */ + function _skipDelimitedStrings($query, $position, $p_position) + { + $ignores = $this->sql_comments; + $ignores[] = $this->string_quoting; + $ignores[] = $this->identifier_quoting; + + foreach ($ignores as $ignore) { + if (!empty($ignore['start'])) { + if (is_int($start_quote = strpos($query, $ignore['start'], $position)) && $start_quote < $p_position) { + $end_quote = $start_quote; + do { + if (!is_int($end_quote = strpos($query, $ignore['end'], $end_quote + 1))) { + if ($ignore['end'] === "\n") { + $end_quote = strlen($query) - 1; + } else { + $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'query with an unterminated text string specified', __FUNCTION__); + return $err; + } + } + } while ($ignore['escape'] && $query[($end_quote - 1)] == $ignore['escape']); + $position = $end_quote + 1; + return $position; + } + } + } + return $position; + } + + // }}} + // {{{ function quote($value, $type = null, $quote = true) + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string text string value that is intended to be converted. + * @param string type to which the value should be converted to + * @param bool quote + * @param bool escape wildcards + * + * @return string text string that represents the given argument value in + * a DBMS specific format. + * + * @access public + */ + function quote($value, $type = null, $quote = true, $escape_wildcards = false) + { + $result = $this->loadModule('Datatype', null, true); + if (PEAR::isError($result)) { + return $result; + } + + return $this->datatype->quote($value, $type, $quote, $escape_wildcards); + } + + // }}} + // {{{ function getDeclaration($type, $name, $field) + + /** + * Obtain DBMS specific SQL code portion needed to declare + * of the given type + * + * @param string type to which the value should be converted to + * @param string name the field to be declared. + * @param string definition of the field + * + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * + * @access public + */ + function getDeclaration($type, $name, $field) + { + $result = $this->loadModule('Datatype', null, true); + if (PEAR::isError($result)) { + return $result; + } + return $this->datatype->getDeclaration($type, $name, $field); + } + + // }}} + // {{{ function compareDefinition($current, $previous) + + /** + * Obtain an array of changes that may need to applied + * + * @param array new definition + * @param array old definition + * + * @return array containing all changes that will need to be applied + * + * @access public + */ + function compareDefinition($current, $previous) + { + $result = $this->loadModule('Datatype', null, true); + if (PEAR::isError($result)) { + return $result; + } + return $this->datatype->compareDefinition($current, $previous); + } + + // }}} + // {{{ function supports($feature) + + /** + * Tell whether a DB implementation or its backend extension + * supports a given feature. + * + * @param string name of the feature (see the MDB2 class doc) + * + * @return bool|string if this DB implementation supports a given feature + * false means no, true means native, + * 'emulated' means emulated + * + * @access public + */ + function supports($feature) + { + if (array_key_exists($feature, $this->supported)) { + return $this->supported[$feature]; + } + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + "unknown support feature $feature", __FUNCTION__); + } + + // }}} + // {{{ function getSequenceName($sqn) + + /** + * adds sequence name formatting to a sequence name + * + * @param string name of the sequence + * + * @return string formatted sequence name + * + * @access public + */ + function getSequenceName($sqn) + { + return sprintf($this->options['seqname_format'], + preg_replace('/[^a-z0-9_\$.]/i', '_', $sqn)); + } + + // }}} + // {{{ function getIndexName($idx) + + /** + * adds index name formatting to a index name + * + * @param string name of the index + * + * @return string formatted index name + * + * @access public + */ + function getIndexName($idx) + { + return sprintf($this->options['idxname_format'], + preg_replace('/[^a-z0-9_\$]/i', '_', $idx)); + } + + // }}} + // {{{ function nextID($seq_name, $ondemand = true) + + /** + * Returns the next free id of a sequence + * + * @param string name of the sequence + * @param bool when true missing sequences are automatic created + * + * @return mixed MDB2 Error Object or id + * + * @access public + */ + function nextID($seq_name, $ondemand = true) + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function lastInsertID($table = null, $field = null) + + /** + * Returns the autoincrement ID if supported or $id or fetches the current + * ID in a sequence called: $table.(empty($field) ? '' : '_'.$field) + * + * @param string name of the table into which a new row was inserted + * @param string name of the field into which a new row was inserted + * + * @return mixed MDB2 Error Object or id + * + * @access public + */ + function lastInsertID($table = null, $field = null) + { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function currID($seq_name) + + /** + * Returns the current id of a sequence + * + * @param string name of the sequence + * + * @return mixed MDB2 Error Object or id + * + * @access public + */ + function currID($seq_name) + { + $this->warnings[] = 'database does not support getting current + sequence value, the sequence value was incremented'; + return $this->nextID($seq_name); + } + + // }}} + // {{{ function queryOne($query, $type = null, $colnum = 0) + + /** + * Execute the specified query, fetch the value from the first column of + * the first row of the result set and then frees + * the result set. + * + * @param string the SELECT query statement to be executed. + * @param string optional argument that specifies the expected + * datatype of the result set field, so that an eventual conversion + * may be performed. The default datatype is text, meaning that no + * conversion is performed + * @param int the column number to fetch + * + * @return mixed MDB2_OK or field value on success, a MDB2 error on failure + * + * @access public + */ + function queryOne($query, $type = null, $colnum = 0) + { + $result = $this->query($query, $type); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $one = $result->fetchOne($colnum); + $result->free(); + return $one; + } + + // }}} + // {{{ function queryRow($query, $types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT) + + /** + * Execute the specified query, fetch the values from the first + * row of the result set into an array and then frees + * the result set. + * + * @param string the SELECT query statement to be executed. + * @param array optional array argument that specifies a list of + * expected datatypes of the result set columns, so that the eventual + * conversions may be performed. The default list of datatypes is + * empty, meaning that no conversion is performed. + * @param int how the array data should be indexed + * + * @return mixed MDB2_OK or data array on success, a MDB2 error on failure + * + * @access public + */ + function queryRow($query, $types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT) + { + $result = $this->query($query, $types); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $row = $result->fetchRow($fetchmode); + $result->free(); + return $row; + } + + // }}} + // {{{ function queryCol($query, $type = null, $colnum = 0) + + /** + * Execute the specified query, fetch the value from the first column of + * each row of the result set into an array and then frees the result set. + * + * @param string the SELECT query statement to be executed. + * @param string optional argument that specifies the expected + * datatype of the result set field, so that an eventual conversion + * may be performed. The default datatype is text, meaning that no + * conversion is performed + * @param int the row number to fetch + * + * @return mixed MDB2_OK or data array on success, a MDB2 error on failure + * + * @access public + */ + function queryCol($query, $type = null, $colnum = 0) + { + $result = $this->query($query, $type); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $col = $result->fetchCol($colnum); + $result->free(); + return $col; + } + + // }}} + // {{{ function queryAll($query, $types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT, $rekey = false, $force_array = false, $group = false) + + /** + * Execute the specified query, fetch all the rows of the result set into + * a two dimensional array and then frees the result set. + * + * @param string the SELECT query statement to be executed. + * @param array optional array argument that specifies a list of + * expected datatypes of the result set columns, so that the eventual + * conversions may be performed. The default list of datatypes is + * empty, meaning that no conversion is performed. + * @param int how the array data should be indexed + * @param bool if set to true, the $all will have the first + * column as its first dimension + * @param bool used only when the query returns exactly + * two columns. If true, the values of the returned array will be + * one-element arrays instead of scalars. + * @param bool if true, the values of the returned array is + * wrapped in another array. If the same key value (in the first + * column) repeats itself, the values will be appended to this array + * instead of overwriting the existing values. + * + * @return mixed MDB2_OK or data array on success, a MDB2 error on failure + * + * @access public + */ + function queryAll($query, $types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT, + $rekey = false, $force_array = false, $group = false) + { + $result = $this->query($query, $types); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $all = $result->fetchAll($fetchmode, $rekey, $force_array, $group); + $result->free(); + return $all; + } + + // }}} +} + +// }}} +// {{{ class MDB2_Result + +/** + * The dummy class that all user space result classes should extend from + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Result +{ +} + +// }}} +// {{{ class MDB2_Result_Common extends MDB2_Result + +/** + * The common result class for MDB2 result objects + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Result_Common extends MDB2_Result +{ + // {{{ Variables (Properties) + + var $db; + var $result; + var $rownum = -1; + var $types = array(); + var $values = array(); + var $offset; + var $offset_count = 0; + var $limit; + var $column_names; + + // }}} + // {{{ constructor: function __construct(&$db, &$result, $limit = 0, $offset = 0) + + /** + * Constructor + */ + function __construct(&$db, &$result, $limit = 0, $offset = 0) + { + $this->db =& $db; + $this->result =& $result; + $this->offset = $offset; + $this->limit = max(0, $limit - 1); + } + + // }}} + // {{{ function MDB2_Result_Common(&$db, &$result, $limit = 0, $offset = 0) + + /** + * PHP 4 Constructor + */ + function MDB2_Result_Common(&$db, &$result, $limit = 0, $offset = 0) + { + $this->__construct($db, $result, $limit, $offset); + } + + // }}} + // {{{ function setResultTypes($types) + + /** + * Define the list of types to be associated with the columns of a given + * result set. + * + * This function may be called before invoking fetchRow(), fetchOne(), + * fetchCol() and fetchAll() so that the necessary data type + * conversions are performed on the data to be retrieved by them. If this + * function is not called, the type of all result set columns is assumed + * to be text, thus leading to not perform any conversions. + * + * @param array variable that lists the + * data types to be expected in the result set columns. If this array + * contains less types than the number of columns that are returned + * in the result set, the remaining columns are assumed to be of the + * type text. Currently, the types clob and blob are not fully + * supported. + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function setResultTypes($types) + { + $load = $this->db->loadModule('Datatype', null, true); + if (PEAR::isError($load)) { + return $load; + } + $types = $this->db->datatype->checkResultTypes($types); + if (PEAR::isError($types)) { + return $types; + } + $this->types = $types; + return MDB2_OK; + } + + // }}} + // {{{ function seek($rownum = 0) + + /** + * Seek to a specific row in a result set + * + * @param int number of the row where the data can be found + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function seek($rownum = 0) + { + $target_rownum = $rownum - 1; + if ($this->rownum > $target_rownum) { + return $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'seeking to previous rows not implemented', __FUNCTION__); + } + while ($this->rownum < $target_rownum) { + $this->fetchRow(); + } + return MDB2_OK; + } + + // }}} + // {{{ function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null) + + /** + * Fetch and return a row of data + * + * @param int how the array data should be indexed + * @param int number of the row where the data can be found + * + * @return int data array on success, a MDB2 error on failure + * + * @access public + */ + function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null) + { + $err =& $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + return $err; + } + + // }}} + // {{{ function fetchOne($colnum = 0) + + /** + * fetch single column from the next row from a result set + * + * @param int the column number to fetch + * @param int number of the row where the data can be found + * + * @return string data on success, a MDB2 error on failure + * + * @access public + */ + function fetchOne($colnum = 0, $rownum = null) + { + $fetchmode = is_numeric($colnum) ? MDB2_FETCHMODE_ORDERED : MDB2_FETCHMODE_ASSOC; + $row = $this->fetchRow($fetchmode, $rownum); + if (!is_array($row) || PEAR::isError($row)) { + return $row; + } + if (!array_key_exists($colnum, $row)) { + return $this->db->raiseError(MDB2_ERROR_TRUNCATED, null, null, + 'column is not defined in the result set: '.$colnum, __FUNCTION__); + } + return $row[$colnum]; + } + + // }}} + // {{{ function fetchCol($colnum = 0) + + /** + * Fetch and return a column from the current row pointer position + * + * @param int the column number to fetch + * + * @return mixed data array on success, a MDB2 error on failure + * + * @access public + */ + function fetchCol($colnum = 0) + { + $column = array(); + $fetchmode = is_numeric($colnum) ? MDB2_FETCHMODE_ORDERED : MDB2_FETCHMODE_ASSOC; + $row = $this->fetchRow($fetchmode); + if (is_array($row)) { + if (!array_key_exists($colnum, $row)) { + return $this->db->raiseError(MDB2_ERROR_TRUNCATED, null, null, + 'column is not defined in the result set: '.$colnum, __FUNCTION__); + } + do { + $column[] = $row[$colnum]; + } while (is_array($row = $this->fetchRow($fetchmode))); + } + if (PEAR::isError($row)) { + return $row; + } + return $column; + } + + // }}} + // {{{ function fetchAll($fetchmode = MDB2_FETCHMODE_DEFAULT, $rekey = false, $force_array = false, $group = false) + + /** + * Fetch and return all rows from the current row pointer position + * + * @param int $fetchmode the fetch mode to use: + * + MDB2_FETCHMODE_ORDERED + * + MDB2_FETCHMODE_ASSOC + * + MDB2_FETCHMODE_ORDERED | MDB2_FETCHMODE_FLIPPED + * + MDB2_FETCHMODE_ASSOC | MDB2_FETCHMODE_FLIPPED + * @param bool if set to true, the $all will have the first + * column as its first dimension + * @param bool used only when the query returns exactly + * two columns. If true, the values of the returned array will be + * one-element arrays instead of scalars. + * @param bool if true, the values of the returned array is + * wrapped in another array. If the same key value (in the first + * column) repeats itself, the values will be appended to this array + * instead of overwriting the existing values. + * + * @return mixed data array on success, a MDB2 error on failure + * + * @access public + * @see getAssoc() + */ + function fetchAll($fetchmode = MDB2_FETCHMODE_DEFAULT, $rekey = false, + $force_array = false, $group = false) + { + $all = array(); + $row = $this->fetchRow($fetchmode); + if (PEAR::isError($row)) { + return $row; + } elseif (!$row) { + return $all; + } + + $shift_array = $rekey ? false : null; + if (!is_null($shift_array)) { + if (is_object($row)) { + $colnum = count(get_object_vars($row)); + } else { + $colnum = count($row); + } + if ($colnum < 2) { + return $this->db->raiseError(MDB2_ERROR_TRUNCATED, null, null, + 'rekey feature requires atleast 2 column', __FUNCTION__); + } + $shift_array = (!$force_array && $colnum == 2); + } + + if ($rekey) { + do { + if (is_object($row)) { + $arr = get_object_vars($row); + $key = reset($arr); + unset($row->{$key}); + } else { + if ($fetchmode & MDB2_FETCHMODE_ASSOC) { + $key = reset($row); + unset($row[key($row)]); + } else { + $key = array_shift($row); + } + if ($shift_array) { + $row = array_shift($row); + } + } + if ($group) { + $all[$key][] = $row; + } else { + $all[$key] = $row; + } + } while (($row = $this->fetchRow($fetchmode))); + } elseif ($fetchmode & MDB2_FETCHMODE_FLIPPED) { + do { + foreach ($row as $key => $val) { + $all[$key][] = $val; + } + } while (($row = $this->fetchRow($fetchmode))); + } else { + do { + $all[] = $row; + } while (($row = $this->fetchRow($fetchmode))); + } + + return $all; + } + + // }}} + // {{{ function rowCount() + /** + * Returns the actual row number that was last fetched (count from 0) + * @return int + * + * @access public + */ + function rowCount() + { + return $this->rownum + 1; + } + + // }}} + // {{{ function numRows() + + /** + * Returns the number of rows in a result object + * + * @return mixed MDB2 Error Object or the number of rows + * + * @access public + */ + function numRows() + { + return $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function nextResult() + + /** + * Move the internal result pointer to the next available result + * + * @return true on success, false if there is no more result set or an error object on failure + * + * @access public + */ + function nextResult() + { + return $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function getColumnNames() + + /** + * Retrieve the names of columns returned by the DBMS in a query result or + * from the cache. + * + * @param bool If set to true the values are the column names, + * otherwise the names of the columns are the keys. + * @return mixed Array variable that holds the names of columns or an + * MDB2 error on failure. + * Some DBMS may not return any columns when the result set + * does not contain any rows. + * + * @access public + */ + function getColumnNames($flip = false) + { + if (!isset($this->column_names)) { + $result = $this->_getColumnNames(); + if (PEAR::isError($result)) { + return $result; + } + $this->column_names = $result; + } + if ($flip) { + return array_flip($this->column_names); + } + return $this->column_names; + } + + // }}} + // {{{ function _getColumnNames() + + /** + * Retrieve the names of columns returned by the DBMS in a query result. + * + * @return mixed Array variable that holds the names of columns as keys + * or an MDB2 error on failure. + * Some DBMS may not return any columns when the result set + * does not contain any rows. + * + * @access private + */ + function _getColumnNames() + { + return $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function numCols() + + /** + * Count the number of columns returned by the DBMS in a query result. + * + * @return mixed integer value with the number of columns, a MDB2 error + * on failure + * + * @access public + */ + function numCols() + { + return $this->db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ function getResource() + + /** + * return the resource associated with the result object + * + * @return resource + * + * @access public + */ + function getResource() + { + return $this->result; + } + + // }}} + // {{{ function bindColumn($column, &$value, $type = null) + + /** + * Set bind variable to a column. + * + * @param int column number or name + * @param mixed variable reference + * @param string specifies the type of the field + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function bindColumn($column, &$value, $type = null) + { + if (!is_numeric($column)) { + $column_names = $this->getColumnNames(); + if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($this->db->options['field_case'] == CASE_LOWER) { + $column = strtolower($column); + } else { + $column = strtoupper($column); + } + } + $column = $column_names[$column]; + } + $this->values[$column] =& $value; + if (!is_null($type)) { + $this->types[$column] = $type; + } + return MDB2_OK; + } + + // }}} + // {{{ function _assignBindColumns($row) + + /** + * Bind a variable to a value in the result row. + * + * @param array row data + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access private + */ + function _assignBindColumns($row) + { + $row = array_values($row); + foreach ($row as $column => $value) { + if (array_key_exists($column, $this->values)) { + $this->values[$column] = $value; + } + } + return MDB2_OK; + } + + // }}} + // {{{ function free() + + /** + * Free the internal resources associated with result. + * + * @return bool true on success, false if result is invalid + * + * @access public + */ + function free() + { + $this->result = false; + return MDB2_OK; + } + + // }}} +} + +// }}} +// {{{ class MDB2_Row + +/** + * The simple class that accepts row data as an array + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Row +{ + // {{{ constructor: function __construct(&$row) + + /** + * constructor + * + * @param resource row data as array + */ + function __construct(&$row) + { + foreach ($row as $key => $value) { + $this->$key = &$row[$key]; + } + } + + // }}} + // {{{ function MDB2_Row(&$row) + + /** + * PHP 4 Constructor + * + * @param resource row data as array + */ + function MDB2_Row(&$row) + { + $this->__construct($row); + } + + // }}} +} + +// }}} +// {{{ class MDB2_Statement_Common + +/** + * The common statement class for MDB2 statement objects + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Statement_Common +{ + // {{{ Variables (Properties) + + var $db; + var $statement; + var $query; + var $result_types; + var $types; + var $values = array(); + var $limit; + var $offset; + var $is_manip; + + // }}} + // {{{ constructor: function __construct(&$db, &$statement, $positions, $query, $types, $result_types, $is_manip = false, $limit = null, $offset = null) + + /** + * Constructor + */ + function __construct(&$db, &$statement, $positions, $query, $types, $result_types, $is_manip = false, $limit = null, $offset = null) + { + $this->db =& $db; + $this->statement =& $statement; + $this->positions = $positions; + $this->query = $query; + $this->types = (array)$types; + $this->result_types = (array)$result_types; + $this->limit = $limit; + $this->is_manip = $is_manip; + $this->offset = $offset; + } + + // }}} + // {{{ function MDB2_Statement_Common(&$db, &$statement, $positions, $query, $types, $result_types, $is_manip = false, $limit = null, $offset = null) + + /** + * PHP 4 Constructor + */ + function MDB2_Statement_Common(&$db, &$statement, $positions, $query, $types, $result_types, $is_manip = false, $limit = null, $offset = null) + { + $this->__construct($db, $statement, $positions, $query, $types, $result_types, $is_manip, $limit, $offset); + } + + // }}} + // {{{ function bindValue($parameter, &$value, $type = null) + + /** + * Set the value of a parameter of a prepared query. + * + * @param int the order number of the parameter in the query + * statement. The order number of the first parameter is 1. + * @param mixed value that is meant to be assigned to specified + * parameter. The type of the value depends on the $type argument. + * @param string specifies the type of the field + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function bindValue($parameter, $value, $type = null) + { + if (!is_numeric($parameter)) { + $parameter = preg_replace('/^:(.*)$/', '\\1', $parameter); + } + if (!in_array($parameter, $this->positions)) { + return $this->db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'Unable to bind to missing placeholder: '.$parameter, __FUNCTION__); + } + $this->values[$parameter] = $value; + if (!is_null($type)) { + $this->types[$parameter] = $type; + } + return MDB2_OK; + } + + // }}} + // {{{ function bindValueArray($values, $types = null) + + /** + * Set the values of multiple a parameter of a prepared query in bulk. + * + * @param array specifies all necessary information + * for bindValue() the array elements must use keys corresponding to + * the number of the position of the parameter. + * @param array specifies the types of the fields + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + * @see bindParam() + */ + function bindValueArray($values, $types = null) + { + $types = is_array($types) ? array_values($types) : array_fill(0, count($values), null); + $parameters = array_keys($values); + foreach ($parameters as $key => $parameter) { + $err = $this->bindValue($parameter, $values[$parameter], $types[$key]); + if (PEAR::isError($err)) { + return $err; + } + } + return MDB2_OK; + } + + // }}} + // {{{ function bindParam($parameter, &$value, $type = null) + + /** + * Bind a variable to a parameter of a prepared query. + * + * @param int the order number of the parameter in the query + * statement. The order number of the first parameter is 1. + * @param mixed variable that is meant to be bound to specified + * parameter. The type of the value depends on the $type argument. + * @param string specifies the type of the field + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function bindParam($parameter, &$value, $type = null) + { + if (!is_numeric($parameter)) { + $parameter = preg_replace('/^:(.*)$/', '\\1', $parameter); + } + if (!in_array($parameter, $this->positions)) { + return $this->db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'Unable to bind to missing placeholder: '.$parameter, __FUNCTION__); + } + $this->values[$parameter] =& $value; + if (!is_null($type)) { + $this->types[$parameter] = $type; + } + return MDB2_OK; + } + + // }}} + // {{{ function bindParamArray(&$values, $types = null) + + /** + * Bind the variables of multiple a parameter of a prepared query in bulk. + * + * @param array specifies all necessary information + * for bindParam() the array elements must use keys corresponding to + * the number of the position of the parameter. + * @param array specifies the types of the fields + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + * @see bindParam() + */ + function bindParamArray(&$values, $types = null) + { + $types = is_array($types) ? array_values($types) : array_fill(0, count($values), null); + $parameters = array_keys($values); + foreach ($parameters as $key => $parameter) { + $err = $this->bindParam($parameter, $values[$parameter], $types[$key]); + if (PEAR::isError($err)) { + return $err; + } + } + return MDB2_OK; + } + + // }}} + // {{{ function &execute($values = null, $result_class = true, $result_wrap_class = false) + + /** + * Execute a prepared query statement. + * + * @param array specifies all necessary information + * for bindParam() the array elements must use keys corresponding to + * the number of the position of the parameter. + * @param mixed specifies which result class to use + * @param mixed specifies which class to wrap results in + * + * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function &execute($values = null, $result_class = true, $result_wrap_class = false) + { + if (is_null($this->positions)) { + return $this->db->raiseError(MDB2_ERROR, null, null, + 'Prepared statement has already been freed', __FUNCTION__); + } + + $values = (array)$values; + if (!empty($values)) { + $err = $this->bindValueArray($values); + if (PEAR::isError($err)) { + return $this->db->raiseError(MDB2_ERROR, null, null, + 'Binding Values failed with message: ' . $err->getMessage(), __FUNCTION__); + } + } + $result =& $this->_execute($result_class, $result_wrap_class); + return $result; + } + + // }}} + // {{{ function &_execute($result_class = true, $result_wrap_class = false) + + /** + * Execute a prepared query statement helper method. + * + * @param mixed specifies which result class to use + * @param mixed specifies which class to wrap results in + * + * @return mixed MDB2_Result or integer on success, a MDB2 error on failure + * + * @access private + */ + function &_execute($result_class = true, $result_wrap_class = false) + { + $this->last_query = $this->query; + $query = ''; + $last_position = 0; + foreach ($this->positions as $current_position => $parameter) { + if (!array_key_exists($parameter, $this->values)) { + return $this->db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'Unable to bind to missing placeholder: '.$parameter, __FUNCTION__); + } + $value = $this->values[$parameter]; + $query.= substr($this->query, $last_position, $current_position - $last_position); + if (!isset($value)) { + $value_quoted = 'NULL'; + } else { + $type = !empty($this->types[$parameter]) ? $this->types[$parameter] : null; + $value_quoted = $this->db->quote($value, $type); + if (PEAR::isError($value_quoted)) { + return $value_quoted; + } + } + $query.= $value_quoted; + $last_position = $current_position + 1; + } + $query.= substr($this->query, $last_position); + + $this->db->offset = $this->offset; + $this->db->limit = $this->limit; + if ($this->is_manip) { + $result = $this->db->exec($query); + } else { + $result =& $this->db->query($query, $this->result_types, $result_class, $result_wrap_class); + } + return $result; + } + + // }}} + // {{{ function free() + + /** + * Release resources allocated for the specified prepared query. + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function free() + { + if (is_null($this->positions)) { + return $this->db->raiseError(MDB2_ERROR, null, null, + 'Prepared statement has already been freed', __FUNCTION__); + } + + $this->statement = null; + $this->positions = null; + $this->query = null; + $this->types = null; + $this->result_types = null; + $this->limit = null; + $this->is_manip = null; + $this->offset = null; + $this->values = null; + + return MDB2_OK; + } + + // }}} +} + +// }}} +// {{{ class MDB2_Module_Common + +/** + * The common modules class for MDB2 module objects + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Module_Common +{ + // {{{ Variables (Properties) + + /** + * contains the key to the global MDB2 instance array of the associated + * MDB2 instance + * + * @var int + * @access protected + */ + var $db_index; + + // }}} + // {{{ constructor: function __construct($db_index) + + /** + * Constructor + */ + function __construct($db_index) + { + $this->db_index = $db_index; + } + + // }}} + // {{{ function MDB2_Module_Common($db_index) + + /** + * PHP 4 Constructor + */ + function MDB2_Module_Common($db_index) + { + $this->__construct($db_index); + } + + // }}} + // {{{ function &getDBInstance() + + /** + * Get the instance of MDB2 associated with the module instance + * + * @return object MDB2 instance or a MDB2 error on failure + * + * @access public + */ + function &getDBInstance() + { + if (isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + $result =& $GLOBALS['_MDB2_databases'][$this->db_index]; + } else { + $result =& MDB2::raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'could not find MDB2 instance'); + } + return $result; + } + + // }}} +} + +// }}} +// {{{ function MDB2_closeOpenTransactions() + +/** + * Close any open transactions form persistent connections + * + * @return void + * + * @access public + */ + +function MDB2_closeOpenTransactions() +{ + reset($GLOBALS['_MDB2_databases']); + while (next($GLOBALS['_MDB2_databases'])) { + $key = key($GLOBALS['_MDB2_databases']); + if ($GLOBALS['_MDB2_databases'][$key]->opened_persistent + && $GLOBALS['_MDB2_databases'][$key]->in_transaction + ) { + $GLOBALS['_MDB2_databases'][$key]->rollback(); + } + } +} + +// }}} +// {{{ function MDB2_defaultDebugOutput(&$db, $scope, $message, $is_manip = null) + +/** + * default debug output handler + * + * @param object reference to an MDB2 database object + * @param string usually the method name that triggered the debug call: + * for example 'query', 'prepare', 'execute', 'parameters', + * 'beginTransaction', 'commit', 'rollback' + * @param string message that should be appended to the debug variable + * @param array contains context information about the debug() call + * common keys are: is_manip, time, result etc. + * + * @return void|string optionally return a modified message, this allows + * rewriting a query before being issued or prepared + * + * @access public + */ +function MDB2_defaultDebugOutput(&$db, $scope, $message, $context = array()) +{ + $db->debug_output.= $scope.'('.$db->db_index.'): '; + $db->debug_output.= $message.$db->getOption('log_line_break'); + return $message; +} + +// }}} +?> \ No newline at end of file diff --git a/MDB2/Date.php b/MDB2/Date.php new file mode 100644 index 0000000..ce84654 --- /dev/null +++ b/MDB2/Date.php @@ -0,0 +1,183 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Date.php,v 1.10 2006/03/01 12:15:32 lsmith Exp $ +// + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +/** + * Several methods to convert the MDB2 native timestamp format (ISO based) + * to and from data structures that are convenient to worth with in side of php. + * For more complex date arithmetic please take a look at the Date package in PEAR + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Date +{ + // {{{ mdbNow() + + /** + * return the current datetime + * + * @return string current datetime in the MDB2 format + * @access public + */ + function mdbNow() + { + return date('Y-m-d H:i:s'); + } + // }}} + + // {{{ mdbToday() + + /** + * return the current date + * + * @return string current date in the MDB2 format + * @access public + */ + function mdbToday() + { + return date('Y-m-d'); + } + // }}} + + // {{{ mdbTime() + + /** + * return the current time + * + * @return string current time in the MDB2 format + * @access public + */ + function mdbTime() + { + return date('H:i:s'); + } + // }}} + + // {{{ date2Mdbstamp() + + /** + * convert a date into a MDB2 timestamp + * + * @param int hour of the date + * @param int minute of the date + * @param int second of the date + * @param int month of the date + * @param int day of the date + * @param int year of the date + * + * @return string a valid MDB2 timestamp + * @access public + */ + function date2Mdbstamp($hour = null, $minute = null, $second = null, + $month = null, $day = null, $year = null) + { + return MDB2_Date::unix2Mdbstamp(mktime($hour, $minute, $second, $month, $day, $year, -1)); + } + // }}} + + // {{{ unix2Mdbstamp() + + /** + * convert a unix timestamp into a MDB2 timestamp + * + * @param int a valid unix timestamp + * + * @return string a valid MDB2 timestamp + * @access public + */ + function unix2Mdbstamp($unix_timestamp) + { + return date('Y-m-d H:i:s', $unix_timestamp); + } + // }}} + + // {{{ mdbstamp2Unix() + + /** + * convert a MDB2 timestamp into a unix timestamp + * + * @param int a valid MDB2 timestamp + * @return string unix timestamp with the time stored in the MDB2 format + * + * @access public + */ + function mdbstamp2Unix($mdb_timestamp) + { + $arr = MDB2_Date::mdbstamp2Date($mdb_timestamp); + + return mktime($arr['hour'], $arr['minute'], $arr['second'], $arr['month'], $arr['day'], $arr['year'], -1); + } + // }}} + + // {{{ mdbstamp2Date() + + /** + * convert a MDB2 timestamp into an array containing all + * values necessary to pass to php's date() function + * + * @param int a valid MDB2 timestamp + * + * @return array with the time split + * @access public + */ + function mdbstamp2Date($mdb_timestamp) + { + list($arr['year'], $arr['month'], $arr['day'], $arr['hour'], $arr['minute'], $arr['second']) = + sscanf($mdb_timestamp, "%04u-%02u-%02u %02u:%02u:%02u"); + return $arr; + } + // }}} +} + +?> diff --git a/MDB2/Driver/Datatype/Common.php b/MDB2/Driver/Datatype/Common.php new file mode 100644 index 0000000..e1b738b --- /dev/null +++ b/MDB2/Driver/Datatype/Common.php @@ -0,0 +1,1837 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.126 2007/03/28 16:49:43 quipo Exp $ + +require_once 'MDB2/LOB.php'; + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +/** + * MDB2_Driver_Common: Base class that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Datatype_Common extends MDB2_Module_Common +{ + var $valid_default_values = array( + 'text' => '', + 'boolean' => true, + 'integer' => 0, + 'decimal' => 0.0, + 'float' => 0.0, + 'timestamp' => '1970-01-01 00:00:00', + 'time' => '00:00:00', + 'date' => '1970-01-01', + 'clob' => '', + 'blob' => '', + ); + + /** + * contains all LOB objects created with this MDB2 instance + * @var array + * @access protected + */ + var $lobs = array(); + + // }}} + // {{{ getValidTypes() + + /** + * Get the list of valid types + * + * This function returns an array of valid types as keys with the values + * being possible default values for all native datatypes and mapped types + * for custom datatypes. + * + * @return mixed array on success, a MDB2 error on failure + * @access public + */ + function getValidTypes() + { + $types = $this->valid_default_values; + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (!empty($db->options['datatype_map'])) { + foreach ($db->options['datatype_map'] as $type => $mapped_type) { + if (array_key_exists($mapped_type, $types)) { + $types[$type] = $types[$mapped_type]; + } elseif (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('type' => $type, 'mapped_type' => $mapped_type); + $default = call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + $types[$type] = $default; + } + } + } + return $types; + } + + // }}} + // {{{ checkResultTypes() + + /** + * Define the list of types to be associated with the columns of a given + * result set. + * + * This function may be called before invoking fetchRow(), fetchOne() + * fetchCole() and fetchAll() so that the necessary data type + * conversions are performed on the data to be retrieved by them. If this + * function is not called, the type of all result set columns is assumed + * to be text, thus leading to not perform any conversions. + * + * @param array $types array variable that lists the + * data types to be expected in the result set columns. If this array + * contains less types than the number of columns that are returned + * in the result set, the remaining columns are assumed to be of the + * type text. Currently, the types clob and blob are not fully + * supported. + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function checkResultTypes($types) + { + $types = is_array($types) ? $types : array($types); + foreach ($types as $key => $type) { + if (!isset($this->valid_default_values[$type])) { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (empty($db->options['datatype_map'][$type])) { + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + $type.' for '.$key.' is not a supported column type', __FUNCTION__); + } + } + } + return $types; + } + + // }}} + // {{{ _baseConvertResult() + + /** + * General type conversion method + * + * @param mixed $value reference to a value to be converted + * @param string $type specifies which type to convert to + * @param boolean $rtrim [optional] when TRUE [default], apply rtrim() to text + * @return object an MDB2 error on failure + * @access protected + */ + function _baseConvertResult($value, $type, $rtrim = true) + { + switch ($type) { + case 'text': + if ($rtrim) { + $value = rtrim($value); + } + return $value; + case 'integer': + return intval($value); + case 'boolean': + return !empty($value); + case 'decimal': + return $value; + case 'float': + return doubleval($value); + case 'date': + return $value; + case 'time': + return $value; + case 'timestamp': + return $value; + case 'clob': + case 'blob': + $this->lobs[] = array( + 'buffer' => null, + 'position' => 0, + 'lob_index' => null, + 'endOfLOB' => false, + 'resource' => $value, + 'value' => null, + 'loaded' => false, + ); + end($this->lobs); + $lob_index = key($this->lobs); + $this->lobs[$lob_index]['lob_index'] = $lob_index; + return fopen('MDB2LOB://'.$lob_index.'@'.$this->db_index, 'r+'); + } + + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_INVALID, null, null, + 'attempt to convert result value to an unknown type :' . $type, __FUNCTION__); + } + + // }}} + // {{{ convertResult() + + /** + * Convert a value to a RDBMS indipendent MDB2 type + * + * @param mixed $value value to be converted + * @param string $type specifies which type to convert to + * @param boolean $rtrim [optional] when TRUE [default], apply rtrim() to text + * @return mixed converted value + * @access public + */ + function convertResult($value, $type, $rtrim = true) + { + if (is_null($value)) { + return null; + } + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (!empty($db->options['datatype_map'][$type])) { + $type = $db->options['datatype_map'][$type]; + if (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('type' => $type, 'value' => $value, 'rtrim' => $rtrim); + return call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + } + } + return $this->_baseConvertResult($value, $type, $rtrim); + } + + // }}} + // {{{ convertResultRow() + + /** + * Convert a result row + * + * @param array $types + * @param array $row specifies the types to convert to + * @param boolean $rtrim [optional] when TRUE [default], apply rtrim() to text + * @return mixed MDB2_OK on success, an MDB2 error on failure + * @access public + */ + function convertResultRow($types, $row, $rtrim = true) + { + $types = $this->_sortResultFieldTypes(array_keys($row), $types); + foreach ($row as $key => $value) { + if (empty($types[$key])) { + continue; + } + $value = $this->convertResult($row[$key], $types[$key], $rtrim); + if (PEAR::isError($value)) { + return $value; + } + $row[$key] = $value; + } + return $row; + } + + // }}} + // {{{ _sortResultFieldTypes() + + /** + * convert a result row + * + * @param array $types + * @param array $row specifies the types to convert to + * @param bool $rtrim if to rtrim text values or not + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function _sortResultFieldTypes($columns, $types) + { + $n_cols = count($columns); + $n_types = count($types); + if ($n_cols > $n_types) { + for ($i= $n_cols - $n_types; $i >= 0; $i--) { + $types[] = null; + } + } + $sorted_types = array(); + foreach ($columns as $col) { + $sorted_types[$col] = null; + } + foreach ($types as $name => $type) { + if (array_key_exists($name, $sorted_types)) { + $sorted_types[$name] = $type; + unset($types[$name]); + } + } + // if there are left types in the array, fill the null values of the + // sorted array with them, in order. + if (count($types)) { + reset($types); + foreach (array_keys($sorted_types) as $k) { + if (is_null($sorted_types[$k])) { + $sorted_types[$k] = current($types); + next($types); + } + } + } + return $sorted_types; + } + + // }}} + // {{{ getDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare + * of the given type + * + * @param string $type type to which the value should be converted to + * @param string $name name the field to be declared. + * @param string $field definition of the field + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access public + */ + function getDeclaration($type, $name, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (!empty($db->options['datatype_map'][$type])) { + $type = $db->options['datatype_map'][$type]; + if (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('type' => $type, 'name' => $name, 'field' => $field); + return call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + } + $field['type'] = $type; + } + + if (!method_exists($this, "_get{$type}Declaration")) { + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'type not defined: '.$type, __FUNCTION__); + } + return $this->{"_get{$type}Declaration"}($name, $field); + } + + // }}} + // {{{ getTypeDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an text type + * field to be used in statements like CREATE TABLE. + * + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access public + */ + function getTypeDeclaration($field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + switch ($field['type']) { + case 'text': + $length = !empty($field['length']) ? $field['length'] : $db->options['default_text_field_length']; + $fixed = !empty($field['fixed']) ? $field['fixed'] : false; + return $fixed ? ($length ? 'CHAR('.$length.')' : 'CHAR('.$db->options['default_text_field_length'].')') + : ($length ? 'VARCHAR('.$length.')' : 'TEXT'); + case 'clob': + return 'TEXT'; + case 'blob': + return 'TEXT'; + case 'integer': + return 'INT'; + case 'boolean': + return 'INT'; + case 'date': + return 'CHAR ('.strlen('YYYY-MM-DD').')'; + case 'time': + return 'CHAR ('.strlen('HH:MM:SS').')'; + case 'timestamp': + return 'CHAR ('.strlen('YYYY-MM-DD HH:MM:SS').')'; + case 'float': + return 'TEXT'; + case 'decimal': + return 'TEXT'; + } + return ''; + } + + // }}} + // {{{ _getDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a generic type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field, or a MDB2_Error on failure + * @access protected + */ + function _getDeclaration($name, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $name = $db->quoteIdentifier($name, true); + $declaration_options = $db->datatype->_getDeclarationOptions($field); + if (PEAR::isError($declaration_options)) { + return $declaration_options; + } + return $name.' '.$this->getTypeDeclaration($field).$declaration_options; + } + + // }}} + // {{{ _getDeclarationOptions() + + /** + * Obtain DBMS specific SQL code portion needed to declare a generic type + * field to be used in statement like CREATE TABLE, without the field name + * and type values (ie. just the character set, default value, if the + * field is permitted to be NULL or not, and the collation options). + * + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Text value to be used as default for this field. + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field's options. + * @access protected + */ + function _getDeclarationOptions($field) + { + $charset = empty($field['charset']) ? '' : + ' '.$this->_getCharsetFieldDeclaration($field['charset']); + + $default = ''; + if (array_key_exists('default', $field)) { + if ($field['default'] === '') { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (empty($field['notnull'])) { + $field['default'] = null; + } else { + $valid_default_values = $this->getValidTypes(); + $field['default'] = $valid_default_values[$field['type']]; + } + if ($field['default'] === '' + && ($db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL) + ) { + $field['default'] = ' '; + } + } + $default = ' DEFAULT '.$this->quote($field['default'], $field['type']); + } elseif (empty($field['notnull'])) { + $default = ' DEFAULT NULL'; + } + + $notnull = empty($field['notnull']) ? '' : ' NOT NULL'; + + $collation = empty($field['collation']) ? '' : + ' '.$this->_getCollationFieldDeclaration($field['collation']); + return $charset.$default.$notnull.$collation; + } + + // }}} + // {{{ _getCharsetFieldDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + function _getCharsetFieldDeclaration($charset) + { + return ''; + } + + // }}} + // {{{ _getCollationFieldDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + function _getCollationFieldDeclaration($collation) + { + return ''; + } + + // }}} + // {{{ _getIntegerDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an integer type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * unsigned + * Boolean flag that indicates whether the field should be + * declared as unsigned integer if possible. + * + * default + * Integer value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getIntegerDeclaration($name, $field) + { + if (!empty($field['unsigned'])) { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $db->warnings[] = "unsigned integer field \"$name\" is being declared as signed integer"; + } + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getTextDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an text type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getTextDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getCLOBDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an character + * large object type field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the large + * object field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access public + */ + function _getCLOBDeclaration($name, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $notnull = empty($field['notnull']) ? '' : ' NOT NULL'; + $name = $db->quoteIdentifier($name, true); + return $name.' '.$this->getTypeDeclaration($field).$notnull; + } + + // }}} + // {{{ _getBLOBDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an binary large + * object type field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the large + * object field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getBLOBDeclaration($name, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $notnull = empty($field['notnull']) ? '' : ' NOT NULL'; + $name = $db->quoteIdentifier($name, true); + return $name.' '.$this->getTypeDeclaration($field).$notnull; + } + + // }}} + // {{{ _getBooleanDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a boolean type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Boolean value to be used as default for this field. + * + * notnullL + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getBooleanDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getDateDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a date type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Date value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getDateDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getTimestampDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a timestamp + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Timestamp value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getTimestampDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getTimeDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a time + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Time value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getTimeDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getFloatDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a float type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Float value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getFloatDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ _getDecimalDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare a decimal type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Decimal value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getDecimalDeclaration($name, $field) + { + return $this->_getDeclaration($name, $field); + } + + // }}} + // {{{ compareDefinition() + + /** + * Obtain an array of changes that may need to applied + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access public + */ + function compareDefinition($current, $previous) + { + $type = !empty($current['type']) ? $current['type'] : null; + + if (!method_exists($this, "_compare{$type}Definition")) { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('current' => $current, 'previous' => $previous); + $change = call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + return $change; + } + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'type "'.$current['type'].'" is not yet supported', __FUNCTION__); + } + + if (empty($previous['type']) || $previous['type'] != $type) { + return $current; + } + + $change = $this->{"_compare{$type}Definition"}($current, $previous); + + if ($previous['type'] != $type) { + $change['type'] = true; + } + + $previous_notnull = !empty($previous['notnull']) ? $previous['notnull'] : false; + $notnull = !empty($current['notnull']) ? $current['notnull'] : false; + if ($previous_notnull != $notnull) { + $change['notnull'] = true; + } + + $previous_default = array_key_exists('default', $previous) ? $previous['default'] : + ($previous_notnull ? '' : null); + $default = array_key_exists('default', $current) ? $current['default'] : + ($notnull ? '' : null); + if ($previous_default !== $default) { + $change['default'] = true; + } + + return $change; + } + + // }}} + // {{{ _compareIntegerDefinition() + + /** + * Obtain an array of changes that may need to applied to an integer field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareIntegerDefinition($current, $previous) + { + $change = array(); + $previous_unsigned = !empty($previous['unsigned']) ? $previous['unsigned'] : false; + $unsigned = !empty($current['unsigned']) ? $current['unsigned'] : false; + if ($previous_unsigned != $unsigned) { + $change['unsigned'] = true; + } + $previous_autoincrement = !empty($previous['autoincrement']) ? $previous['autoincrement'] : false; + $autoincrement = !empty($current['autoincrement']) ? $current['autoincrement'] : false; + if ($previous_autoincrement != $autoincrement) { + $change['autoincrement'] = true; + } + return $change; + } + + // }}} + // {{{ _compareTextDefinition() + + /** + * Obtain an array of changes that may need to applied to an text field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareTextDefinition($current, $previous) + { + $change = array(); + $previous_length = !empty($previous['length']) ? $previous['length'] : 0; + $length = !empty($current['length']) ? $current['length'] : 0; + if ($previous_length != $length) { + $change['length'] = true; + } + $previous_fixed = !empty($previous['fixed']) ? $previous['fixed'] : 0; + $fixed = !empty($current['fixed']) ? $current['fixed'] : 0; + if ($previous_fixed != $fixed) { + $change['fixed'] = true; + } + return $change; + } + + // }}} + // {{{ _compareCLOBDefinition() + + /** + * Obtain an array of changes that may need to applied to an CLOB field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareCLOBDefinition($current, $previous) + { + return $this->_compareTextDefinition($current, $previous); + } + + // }}} + // {{{ _compareBLOBDefinition() + + /** + * Obtain an array of changes that may need to applied to an BLOB field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareBLOBDefinition($current, $previous) + { + return $this->_compareTextDefinition($current, $previous); + } + + // }}} + // {{{ _compareDateDefinition() + + /** + * Obtain an array of changes that may need to applied to an date field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareDateDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ _compareTimeDefinition() + + /** + * Obtain an array of changes that may need to applied to an time field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareTimeDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ _compareTimestampDefinition() + + /** + * Obtain an array of changes that may need to applied to an timestamp field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareTimestampDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ _compareBooleanDefinition() + + /** + * Obtain an array of changes that may need to applied to an boolean field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareBooleanDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ _compareFloatDefinition() + + /** + * Obtain an array of changes that may need to applied to an float field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareFloatDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ _compareDecimalDefinition() + + /** + * Obtain an array of changes that may need to applied to an decimal field + * + * @param array $current new definition + * @param array $previous old definition + * @return array containing all changes that will need to be applied + * @access protected + */ + function _compareDecimalDefinition($current, $previous) + { + return array(); + } + + // }}} + // {{{ quote() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param string $type type to which the value should be converted to + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access public + */ + function quote($value, $type = null, $quote = true, $escape_wildcards = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (is_null($value) + || ($value === '' && $db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL) + ) { + if (!$quote) { + return null; + } + return 'NULL'; + } + + if (is_null($type)) { + switch (gettype($value)) { + case 'integer': + $type = 'integer'; + break; + case 'double': + // todo: default to decimal as float is quite unusual + // $type = 'float'; + $type = 'decimal'; + break; + case 'boolean': + $type = 'boolean'; + break; + case 'array': + $value = serialize($value); + case 'object': + $type = 'text'; + break; + default: + if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $value)) { + $type = 'timestamp'; + } elseif (preg_match('/^\d{2}:\d{2}$/', $value)) { + $type = 'time'; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + $type = 'date'; + } else { + $type = 'text'; + } + break; + } + } elseif (!empty($db->options['datatype_map'][$type])) { + $type = $db->options['datatype_map'][$type]; + if (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('type' => $type, 'value' => $value, 'quote' => $quote, 'escape_wildcards' => $escape_wildcards); + return call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + } + } + + if (!method_exists($this, "_quote{$type}")) { + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'type not defined: '.$type, __FUNCTION__); + } + $value = $this->{"_quote{$type}"}($value, $quote, $escape_wildcards); + if ($quote && $escape_wildcards && $db->string_quoting['escape_pattern'] + && $db->string_quoting['escape'] !== $db->string_quoting['escape_pattern'] + ) { + $value.= $this->patternEscapeString(); + } + return $value; + } + + // }}} + // {{{ _quoteInteger() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteInteger($value, $quote, $escape_wildcards) + { + return (int)$value; + } + + // }}} + // {{{ _quoteText() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that already contains any DBMS specific + * escaped character sequences. + * @access protected + */ + function _quoteText($value, $quote, $escape_wildcards) + { + if (!$quote) { + return $value; + } + + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $value = $db->escape($value, $escape_wildcards); + if (PEAR::isError($value)) { + return $value; + } + return "'".$value."'"; + } + + // }}} + // {{{ _readFile() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _readFile($value) + { + $close = false; + if (preg_match('/^(\w+:\/\/)(.*)$/', $value, $match)) { + $close = true; + if ($match[1] == 'file://') { + $value = $match[2]; + } + $value = @fopen($value, 'r'); + } + + if (is_resource($value)) { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $fp = $value; + $value = ''; + while (!@feof($fp)) { + $value.= @fread($fp, $db->options['lob_buffer_length']); + } + if ($close) { + @fclose($fp); + } + } + + return $value; + } + + // }}} + // {{{ _quoteLOB() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteLOB($value, $quote, $escape_wildcards) + { + $value = $this->_readFile($value); + if (PEAR::isError($value)) { + return $value; + } + return $this->_quoteText($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteCLOB() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteCLOB($value, $quote, $escape_wildcards) + { + return $this->_quoteLOB($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteBLOB() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteBLOB($value, $quote, $escape_wildcards) + { + return $this->_quoteLOB($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteBoolean() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteBoolean($value, $quote, $escape_wildcards) + { + return ($value ? 1 : 0); + } + + // }}} + // {{{ _quoteDate() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteDate($value, $quote, $escape_wildcards) + { + if ($value === 'CURRENT_DATE') { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (isset($db->function) && is_a($db->function, 'MDB2_Driver_Function_Common')) { + return $db->function->now('date'); + } + return 'CURRENT_DATE'; + } + return $this->_quoteText($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteTimestamp() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteTimestamp($value, $quote, $escape_wildcards) + { + if ($value === 'CURRENT_TIMESTAMP') { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (isset($db->function) && is_a($db->function, 'MDB2_Driver_Function_Common')) { + return $db->function->now('timestamp'); + } + return 'CURRENT_TIMESTAMP'; + } + return $this->_quoteText($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteTime() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteTime($value, $quote, $escape_wildcards) + { + if ($value === 'CURRENT_TIME') { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if (isset($db->function) && is_a($db->function, 'MDB2_Driver_Function_Common')) { + return $db->function->now('time'); + } + return 'CURRENT_TIME'; + } + return $this->_quoteText($value, $quote, $escape_wildcards); + } + + // }}} + // {{{ _quoteFloat() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteFloat($value, $quote, $escape_wildcards) + { + if (preg_match('/^(.*)e([-+])(\d+)$/i', $value, $matches)) { + $decimal = $this->_quoteDecimal($matches[1], $quote, $escape_wildcards); + $sign = $matches[2]; + $exponent = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + $value = $decimal.'E'.$sign.$exponent; + } else { + $value = $this->_quoteDecimal($value, $quote, $escape_wildcards); + } + return $value; + } + + // }}} + // {{{ _quoteDecimal() + + /** + * Convert a text value into a DBMS specific format that is suitable to + * compose query statements. + * + * @param string $value text string value that is intended to be converted. + * @param bool $quote determines if the value should be quoted and escaped + * @param bool $escape_wildcards if to escape escape wildcards + * @return string text string that represents the given argument value in + * a DBMS specific format. + * @access protected + */ + function _quoteDecimal($value, $quote, $escape_wildcards) + { + $value = (string)$value; + $value = preg_replace('/[^\d\.,\-+eE]/', '', $value); + if (preg_match('/[^.0-9]/', $value)) { + if (strpos($value, ',')) { + // 1000,00 + if (!strpos($value, '.')) { + // convert the last "," to a "." + $value = strrev(str_replace(',', '.', strrev($value))); + // 1.000,00 + } elseif (strpos($value, '.') && strpos($value, '.') < strpos($value, ',')) { + $value = str_replace('.', '', $value); + // convert the last "," to a "." + $value = strrev(str_replace(',', '.', strrev($value))); + // 1,000.00 + } else { + $value = str_replace(',', '', $value); + } + } + } + return $value; + } + + // }}} + // {{{ writeLOBToFile() + + /** + * retrieve LOB from the database + * + * @param resource $lob stream handle + * @param string $file name of the file into which the LOb should be fetched + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access protected + */ + function writeLOBToFile($lob, $file) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (preg_match('/^(\w+:\/\/)(.*)$/', $file, $match)) { + if ($match[1] == 'file://') { + $file = $match[2]; + } + } + + $fp = @fopen($file, 'wb'); + while (!@feof($lob)) { + $result = @fread($lob, $db->options['lob_buffer_length']); + $read = strlen($result); + if (@fwrite($fp, $result, $read) != $read) { + @fclose($fp); + return $db->raiseError(MDB2_ERROR, null, null, + 'could not write to the output file', __FUNCTION__); + } + } + @fclose($fp); + return MDB2_OK; + } + + // }}} + // {{{ _retrieveLOB() + + /** + * retrieve LOB from the database + * + * @param array $lob array + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access protected + */ + function _retrieveLOB(&$lob) + { + if (is_null($lob['value'])) { + $lob['value'] = $lob['resource']; + } + $lob['loaded'] = true; + return MDB2_OK; + } + + // }}} + // {{{ readLOB() + + /** + * Read data from large object input stream. + * + * @param resource $lob stream handle + * @param string $data reference to a variable that will hold data + * to be read from the large object input stream + * @param integer $length value that indicates the largest ammount ofdata + * to be read from the large object input stream. + * @return mixed the effective number of bytes read from the large object + * input stream on sucess or an MDB2 error object. + * @access public + * @see endOfLOB() + */ + function _readLOB($lob, $length) + { + return substr($lob['value'], $lob['position'], $length); + } + + // }}} + // {{{ _endOfLOB() + + /** + * Determine whether it was reached the end of the large object and + * therefore there is no more data to be read for the its input stream. + * + * @param array $lob array + * @return mixed true or false on success, a MDB2 error on failure + * @access protected + */ + function _endOfLOB($lob) + { + return $lob['endOfLOB']; + } + + // }}} + // {{{ destroyLOB() + + /** + * Free any resources allocated during the lifetime of the large object + * handler object. + * + * @param resource $lob stream handle + * @access public + */ + function destroyLOB($lob) + { + $lob_data = stream_get_meta_data($lob); + $lob_index = $lob_data['wrapper_data']->lob_index; + fclose($lob); + if (isset($this->lobs[$lob_index])) { + $this->_destroyLOB($this->lobs[$lob_index]); + unset($this->lobs[$lob_index]); + } + return MDB2_OK; + } + + // }}} + // {{{ _destroyLOB() + + /** + * Free any resources allocated during the lifetime of the large object + * handler object. + * + * @param array $lob array + * @access private + */ + function _destroyLOB(&$lob) + { + return MDB2_OK; + } + + // }}} + // {{{ implodeArray() + + /** + * apply a type to all values of an array and return as a comma seperated string + * useful for generating IN statements + * + * @access public + * + * @param array $array data array + * @param string $type determines type of the field + * + * @return string comma seperated values + */ + function implodeArray($array, $type = false) + { + if (!is_array($array) || empty($array)) { + return 'NULL'; + } + if ($type) { + foreach ($array as $value) { + $return[] = $this->quote($value, $type); + } + } else { + $return = $array; + } + return implode(', ', $return); + } + + // }}} + // {{{ matchPattern() + + /** + * build a pattern matching string + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @access public + * + * @param array $pattern even keys are strings, odd are patterns (% and _) + * @param string $operator optional pattern operator (LIKE, ILIKE and maybe others in the future) + * @param string $field optional field name that is being matched against + * (might be required when emulating ILIKE) + * + * @return string SQL pattern + */ + function matchPattern($pattern, $operator = null, $field = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $match = ''; + if (!is_null($operator)) { + $operator = strtoupper($operator); + switch ($operator) { + // case insensitive + case 'ILIKE': + if (is_null($field)) { + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'case insensitive LIKE matching requires passing the field name', __FUNCTION__); + } + $db->loadModule('Function', null, true); + $match = $db->function->lower($field).' LIKE '; + break; + // case sensitive + case 'LIKE': + $match = is_null($field) ? 'LIKE ' : $field.' LIKE '; + break; + default: + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'not a supported operator type:'. $operator, __FUNCTION__); + } + } + $match.= "'"; + foreach ($pattern as $key => $value) { + if ($key % 2) { + $match.= $value; + } else { + if ($operator === 'ILIKE') { + $value = strtolower($value); + } + $escaped = $db->escape($value); + if (PEAR::isError($escaped)) { + return $escaped; + } + $match.= $db->escapePattern($escaped); + } + } + $match.= "'"; + $match.= $this->patternEscapeString(); + return $match; + } + + // }}} + // {{{ patternEscapeString() + + /** + * build string to define pattern escape character + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @access public + * + * @return string define pattern escape character + */ + function patternEscapeString() + { + return ''; + } + + // }}} + // {{{ mapNativeDatatype() + + /** + * Maps a native array description of a field to a MDB2 datatype and length + * + * @param array $field native field description + * @return array containing the various possible types, length, sign, fixed + * @access public + */ + function mapNativeDatatype($field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + // If the user has specified an option to map the native field + // type to a custom MDB2 datatype... + $db_type = strtok($field['type'], '(), '); + if (!empty($db->options['nativetype_map_callback'][$db_type])) { + return call_user_func_array($db->options['nativetype_map_callback'][$db_type], array($db, $field)); + } + + // Otherwise perform the built-in (i.e. normal) MDB2 native type to + // MDB2 datatype conversion + return $this->_mapNativeDatatype($field); + } + + // }}} + // {{{ _mapNativeDatatype() + + /** + * Maps a native array description of a field to a MDB2 datatype and length + * + * @param array $field native field description + * @return array containing the various possible types, length, sign, fixed + * @access public + */ + function _mapNativeDatatype($field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ mapPrepareDatatype() + + /** + * Maps an mdb2 datatype to mysqli prepare type + * + * @param string $type + * @return string + * @access public + */ + function mapPrepareDatatype($type) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (!empty($db->options['datatype_map'][$type])) { + $type = $db->options['datatype_map'][$type]; + if (!empty($db->options['datatype_map_callback'][$type])) { + $parameter = array('type' => $type); + return call_user_func_array($db->options['datatype_map_callback'][$type], array(&$db, __FUNCTION__, $parameter)); + } + } + + return $type; + } +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Datatype/mysql.php b/MDB2/Driver/Datatype/mysql.php new file mode 100644 index 0000000..34db8f3 --- /dev/null +++ b/MDB2/Driver/Datatype/mysql.php @@ -0,0 +1,471 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.60 2007/03/28 16:58:54 quipo Exp $ +// + +require_once 'MDB2/Driver/Datatype/Common.php'; + +/** + * MDB2 MySQL driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Datatype_mysql extends MDB2_Driver_Datatype_Common +{ + // {{{ _getCharsetFieldDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + function _getCharsetFieldDeclaration($charset) + { + return 'CHARACTER SET '.$charset; + } + + // }}} + // {{{ _getCollationFieldDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + function _getCollationFieldDeclaration($collation) + { + return 'COLLATE '.$collation; + } + + // }}} + // {{{ getTypeDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an text type + * field to be used in statements like CREATE TABLE. + * + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access public + */ + function getTypeDeclaration($field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + switch ($field['type']) { + case 'text': + if (empty($field['length']) && array_key_exists('default', $field)) { + $field['length'] = $db->varchar_max_length; + } + $length = !empty($field['length']) ? $field['length'] : false; + $fixed = !empty($field['fixed']) ? $field['fixed'] : false; + return $fixed ? ($length ? 'CHAR('.$length.')' : 'CHAR(255)') + : ($length ? 'VARCHAR('.$length.')' : 'TEXT'); + case 'clob': + if (!empty($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYTEXT'; + } elseif ($length <= 65532) { + return 'TEXT'; + } elseif ($length <= 16777215) { + return 'MEDIUMTEXT'; + } + } + return 'LONGTEXT'; + case 'blob': + if (!empty($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYBLOB'; + } elseif ($length <= 65532) { + return 'BLOB'; + } elseif ($length <= 16777215) { + return 'MEDIUMBLOB'; + } + } + return 'LONGBLOB'; + case 'integer': + if (!empty($field['length'])) { + $length = $field['length']; + if ($length <= 1) { + return 'TINYINT'; + } elseif ($length == 2) { + return 'SMALLINT'; + } elseif ($length == 3) { + return 'MEDIUMINT'; + } elseif ($length == 4) { + return 'INT'; + } elseif ($length > 4) { + return 'BIGINT'; + } + } + return 'INT'; + case 'boolean': + return 'TINYINT(1)'; + case 'date': + return 'DATE'; + case 'time': + return 'TIME'; + case 'timestamp': + return 'DATETIME'; + case 'float': + return 'DOUBLE'; + case 'decimal': + $length = !empty($field['length']) ? $field['length'] : 18; + $scale = !empty($field['scale']) ? $field['scale'] : $db->options['decimal_places']; + return 'DECIMAL('.$length.','.$scale.')'; + } + return ''; + } + + // }}} + // {{{ _getIntegerDeclaration() + + /** + * Obtain DBMS specific SQL code portion needed to declare an integer type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param string $field associative array with the name of the properties + * of the field being declared as array indexes. + * Currently, the types of supported field + * properties are as follows: + * + * unsigned + * Boolean flag that indicates whether the field + * should be declared as unsigned integer if + * possible. + * + * default + * Integer value to be used as default for this + * field. + * + * notnull + * Boolean flag that indicates whether this field is + * constrained to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @access protected + */ + function _getIntegerDeclaration($name, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $default = $autoinc = ''; + if (!empty($field['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT PRIMARY KEY'; + } elseif (array_key_exists('default', $field)) { + if ($field['default'] === '') { + $field['default'] = empty($field['notnull']) ? null : 0; + } + $default = ' DEFAULT '.$this->quote($field['default'], 'integer'); + } elseif (empty($field['notnull'])) { + $default = ' DEFAULT NULL'; + } + + $notnull = empty($field['notnull']) ? '' : ' NOT NULL'; + $unsigned = empty($field['unsigned']) ? '' : ' UNSIGNED'; + $name = $db->quoteIdentifier($name, true); + return $name.' '.$this->getTypeDeclaration($field).$unsigned.$default.$notnull.$autoinc; + } + + // }}} + // {{{ matchPattern() + + /** + * build a pattern matching string + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @access public + * + * @param array $pattern even keys are strings, odd are patterns (% and _) + * @param string $operator optional pattern operator (LIKE, ILIKE and maybe others in the future) + * @param string $field optional field name that is being matched against + * (might be required when emulating ILIKE) + * + * @return string SQL pattern + */ + function matchPattern($pattern, $operator = null, $field = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $match = ''; + if (!is_null($operator)) { + $field = is_null($field) ? '' : $field.' '; + $operator = strtoupper($operator); + switch ($operator) { + // case insensitive + case 'ILIKE': + $match = $field.'LIKE '; + break; + // case sensitive + case 'LIKE': + $match = $field.'LIKE BINARY '; + break; + default: + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'not a supported operator type:'. $operator, __FUNCTION__); + } + } + $match.= "'"; + foreach ($pattern as $key => $value) { + if ($key % 2) { + $match.= $value; + } else { + $match.= $db->escapePattern($db->escape($value)); + } + } + $match.= "'"; + $match.= $this->patternEscapeString(); + return $match; + } + + // }}} + // {{{ _mapNativeDatatype() + + /** + * Maps a native array description of a field to a MDB2 datatype and length + * + * @param array $field native field description + * @return array containing the various possible types, length, sign, fixed + * @access public + */ + function _mapNativeDatatype($field) + { + $db_type = strtolower($field['type']); + $db_type = strtok($db_type, '(), '); + if ($db_type == 'national') { + $db_type = strtok('(), '); + } + if (!empty($field['length'])) { + $length = strtok($field['length'], ', '); + $decimal = strtok(', '); + } else { + $length = strtok('(), '); + $decimal = strtok('(), '); + } + $type = array(); + $unsigned = $fixed = null; + switch ($db_type) { + case 'tinyint': + $type[] = 'integer'; + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 1; + break; + case 'smallint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 2; + break; + case 'mediumint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 3; + break; + case 'int': + case 'integer': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 4; + break; + case 'bigint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 8; + break; + case 'tinytext': + case 'mediumtext': + case 'longtext': + case 'text': + case 'text': + case 'varchar': + $fixed = false; + case 'string': + case 'char': + $type[] = 'text'; + if ($length == '1') { + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + } elseif (strstr($db_type, 'text')) { + $type[] = 'clob'; + if ($decimal == 'binary') { + $type[] = 'blob'; + } + } + if ($fixed !== false) { + $fixed = true; + } + break; + case 'enum': + $type[] = 'text'; + preg_match_all('/\'.+\'/U', $field['type'], $matches); + $length = 0; + $fixed = false; + if (is_array($matches)) { + foreach ($matches[0] as $value) { + $length = max($length, strlen($value)-2); + } + if ($length == '1' && count($matches[0]) == 2) { + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + } + } + $type[] = 'integer'; + case 'set': + $fixed = false; + $type[] = 'text'; + $type[] = 'integer'; + break; + case 'date': + $type[] = 'date'; + $length = null; + break; + case 'datetime': + case 'timestamp': + $type[] = 'timestamp'; + $length = null; + break; + case 'time': + $type[] = 'time'; + $length = null; + break; + case 'float': + case 'double': + case 'real': + $type[] = 'float'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + break; + case 'unknown': + case 'decimal': + case 'numeric': + $type[] = 'decimal'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + if ($decimal !== false) { + $length = $length.','.$decimal; + } + break; + case 'tinyblob': + case 'mediumblob': + case 'longblob': + case 'blob': + $type[] = 'blob'; + $length = null; + break; + case 'binary': + case 'varbinary': + $type[] = 'blob'; + break; + case 'year': + $type[] = 'integer'; + $type[] = 'date'; + $length = null; + break; + default: + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'unknown database attribute type: '.$db_type, __FUNCTION__); + } + + if ((int)$length <= 0) { + $length = null; + } + + return array($type, $length, $unsigned, $fixed); + } + + // }}} +} + +?> \ No newline at end of file diff --git a/MDB2/Driver/Function/Common.php b/MDB2/Driver/Function/Common.php new file mode 100644 index 0000000..3e582c6 --- /dev/null +++ b/MDB2/Driver/Function/Common.php @@ -0,0 +1,231 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.17 2007/01/12 11:29:12 quipo Exp $ +// + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +/** + * Base class for the function modules that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Function_Common extends MDB2_Module_Common +{ + // {{{ executeStoredProc() + + /** + * Execute a stored procedure and return any results + * + * @param string $name string that identifies the function to execute + * @param mixed $params array that contains the paramaters to pass the stored proc + * @param mixed $types array that contains the types of the columns in + * the result set + * @param mixed $result_class string which specifies which result class to use + * @param mixed $result_wrap_class string which specifies which class to wrap results in + * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function &executeStoredProc($name, $params = null, $types = null, $result_class = true, $result_wrap_class = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $error =& $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + return $error; + } + + // }}} + // {{{ functionTable() + + /** + * return string for internal table used when calling only a function + * + * @return string for internal table used when calling only a function + * @access public + */ + function functionTable() + { + return ''; + } + + // }}} + // {{{ now() + + /** + * Return string to call a variable with the current timestamp inside an SQL statement + * There are three special variables for current date and time: + * - CURRENT_TIMESTAMP (date and time, TIMESTAMP type) + * - CURRENT_DATE (date, DATE type) + * - CURRENT_TIME (time, TIME type) + * + * @return string to call a variable with the current timestamp + * @access public + */ + function now($type = 'timestamp') + { + switch ($type) { + case 'time': + return 'CURRENT_TIME'; + case 'date': + return 'CURRENT_DATE'; + case 'timestamp': + default: + return 'CURRENT_TIMESTAMP'; + } + } + + // }}} + // {{{ substring() + + /** + * return string to call a function to get a substring inside an SQL statement + * + * @return string to call a function to get a substring + * @access public + */ + function substring($value, $position = 1, $length = null) + { + if (!is_null($length)) { + return "SUBSTRING($value FROM $position FOR $length)"; + } + return "SUBSTRING($value FROM $position)"; + } + + // }}} + // {{{ concat() + + /** + * Returns string to concatenate two or more string parameters + * + * @param string $value1 + * @param string $value2 + * @param string $values... + * @return string to concatenate two strings + * @access public + */ + function concat($value1, $value2) + { + $args = func_get_args(); + return "(".implode(' || ', $args).")"; + } + + // }}} + // {{{ random() + + /** + * return string to call a function to get random value inside an SQL statement + * + * @return return string to generate float between 0 and 1 + * @access public + */ + function random() + { + return 'RAND()'; + } + + // }}} + // {{{ lower() + + /** + * return string to call a function to lower the case of an expression + * + * @param string $expression + * @return return string to lower case of an expression + * @access public + */ + function lower($expression) + { + return "LOWER($expression)"; + } + + // }}} + // {{{ upper() + + /** + * return string to call a function to upper the case of an expression + * + * @param string $expression + * @return return string to upper case of an expression + * @access public + */ + function upper($expression) + { + return "UPPER($expression)"; + } + + // }}} + // {{{ guid() + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + * @access public + */ + function guid() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $error =& $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + return $error; + } + + // }}} +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Function/mysql.php b/MDB2/Driver/Function/mysql.php new file mode 100644 index 0000000..90dea81 --- /dev/null +++ b/MDB2/Driver/Function/mysql.php @@ -0,0 +1,120 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.11 2007/01/12 11:29:12 quipo Exp $ +// + +require_once 'MDB2/Driver/Function/Common.php'; + +/** + * MDB2 MySQL driver for the function modules + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Function_mysql extends MDB2_Driver_Function_Common +{ + // }}} + // {{{ executeStoredProc() + + /** + * Execute a stored procedure and return any results + * + * @param string $name string that identifies the function to execute + * @param mixed $params array that contains the paramaters to pass the stored proc + * @param mixed $types array that contains the types of the columns in + * the result set + * @param mixed $result_class string which specifies which result class to use + * @param mixed $result_wrap_class string which specifies which class to wrap results in + * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function &executeStoredProc($name, $params = null, $types = null, $result_class = true, $result_wrap_class = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = 'CALL '.$name; + $query .= $params ? '('.implode(', ', $params).')' : '()'; + return $db->query($query, $types, $result_class, $result_wrap_class); + } + + // }}} + // {{{ concat() + + /** + * Returns string to concatenate two or more string parameters + * + * @param string $value1 + * @param string $value2 + * @param string $values... + * @return string to concatenate two strings + * @access public + **/ + function concat($value1, $value2) + { + $args = func_get_args(); + return "CONCAT(".implode(', ', $args).")"; + } + + // }}} + // {{{ guid() + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + * @access public + */ + function guid() + { + return 'UUID()'; + } + + // }}} +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Manager/Common.php b/MDB2/Driver/Manager/Common.php new file mode 100644 index 0000000..89b7078 --- /dev/null +++ b/MDB2/Driver/Manager/Common.php @@ -0,0 +1,864 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.62 2007/03/28 16:39:55 quipo Exp $ +// + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +/** + * Base class for the management modules that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Manager_Common extends MDB2_Module_Common +{ + // {{{ getFieldDeclarationList() + + /** + * Get declaration of a number of field in bulk + * + * @param array $fields a multidimensional associative array. + * The first dimension determines the field name, while the second + * dimension is keyed with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * default + * Boolean value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * + * @return mixed string on success, a MDB2 error on failure + * @access public + */ + function getFieldDeclarationList($fields) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (!is_array($fields) || empty($fields)) { + return $db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'missing any fields', __FUNCTION__); + } + foreach ($fields as $field_name => $field) { + $query = $db->getDeclaration($field['type'], $field_name, $field); + if (PEAR::isError($query)) { + return $query; + } + $query_fields[] = $query; + } + return implode(', ', $query_fields); + } + + // }}} + // {{{ _fixSequenceName() + + /** + * Removes any formatting in an sequence name using the 'seqname_format' option + * + * @param string $sqn string that containts name of a potential sequence + * @param bool $check if only formatted sequences should be returned + * @return string name of the sequence with possible formatting removed + * @access protected + */ + function _fixSequenceName($sqn, $check = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $seq_pattern = '/^'.preg_replace('/%s/', '([a-z0-9_]+)', $db->options['seqname_format']).'$/i'; + $seq_name = preg_replace($seq_pattern, '\\1', $sqn); + if ($seq_name && !strcasecmp($sqn, $db->getSequenceName($seq_name))) { + return $seq_name; + } + if ($check) { + return false; + } + return $sqn; + } + + // }}} + // {{{ _fixIndexName() + + /** + * Removes any formatting in an index name using the 'idxname_format' option + * + * @param string $idx string that containts name of anl index + * @return string name of the index with possible formatting removed + * @access protected + */ + function _fixIndexName($idx) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $idx_pattern = '/^'.preg_replace('/%s/', '([a-z0-9_]+)', $db->options['idxname_format']).'$/i'; + $idx_name = preg_replace($idx_pattern, '\\1', $idx); + if ($idx_name && !strcasecmp($idx, $db->getIndexName($idx_name))) { + return $idx_name; + } + return $idx; + } + + // }}} + // {{{ createDatabase() + + /** + * create a new database + * + * @param string $name name of the database that should be created + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createDatabase($database) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ dropDatabase() + + /** + * drop an existing database + * + * @param string $name name of the database that should be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropDatabase($database) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ _getCreateTableQuery() + + /** + * Create a basic SQL query for a new table creation + * @param string $name Name of the database that should be created + * @param array $fields Associative array that contains the definition of each field of the new table + * @param array $options An associative array of table options + * @return mixed string (the SQL query) on success, a MDB2 error on failure + * @see createTable() + */ + function _getCreateTableQuery($name, $fields, $options = array()) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (!$name) { + return $db->raiseError(MDB2_ERROR_CANNOT_CREATE, null, null, + 'no valid table name specified', __FUNCTION__); + } + if (empty($fields)) { + return $db->raiseError(MDB2_ERROR_CANNOT_CREATE, null, null, + 'no fields specified for table "'.$name.'"', __FUNCTION__); + } + $query_fields = $this->getFieldDeclarationList($fields); + if (PEAR::isError($query_fields)) { + return $query_fields; + } + if (!empty($options['primary'])) { + $query_fields.= ', PRIMARY KEY ('.implode(', ', array_keys($options['primary'])).')'; + } + + $name = $db->quoteIdentifier($name, true); + $result = 'CREATE '; + if (!empty($options['temporary'])) { + $result .= $this->_getTemporaryTableQuery(); + } + $result .= " TABLE $name ($query_fields)"; + return $result; + } + + // }}} + // {{{ _getTemporaryTableQuery() + + /** + * A method to return the required SQL string that fits between CREATE ... TABLE + * to create the table as a temporary table. + * + * Should be overridden in driver classes to return the correct string for the + * specific database type. + * + * The default is to return the string "TEMPORARY" - this will result in a + * SQL error for any database that does not support temporary tables, or that + * requires a different SQL command from "CREATE TEMPORARY TABLE". + * + * @return string The string required to be placed between "CREATE" and "TABLE" + * to generate a temporary table, if possible. + */ + function _getTemporaryTableQuery() + { + return 'TEMPORARY'; + } + + // }}} + // {{{ createTable() + + /** + * create a new table + * + * @param string $name Name of the database that should be created + * @param array $fields Associative array that contains the definition of each field of the new table + * The indexes of the array entries are the names of the fields of the table an + * the array entry values are associative arrays like those that are meant to be + * passed with the field definitions to get[Type]Declaration() functions. + * array( + * 'id' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * 'notnull' => 1 + * 'default' => 0 + * ), + * 'name' => array( + * 'type' => 'text', + * 'length' => 12 + * ), + * 'password' => array( + * 'type' => 'text', + * 'length' => 12 + * ) + * ); + * @param array $options An associative array of table options: + * array( + * 'comment' => 'Foo', + * 'temporary' => true|false, + * ); + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createTable($name, $fields, $options = array()) + { + $query = $this->_getCreateTableQuery($name, $fields, $options); + if (PEAR::isError($query)) { + return $query; + } + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + return $db->exec($query); + } + + // }}} + // {{{ dropTable() + + /** + * drop an existing table + * + * @param string $name name of the table that should be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropTable($name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $name = $db->quoteIdentifier($name, true); + return $db->exec("DROP TABLE $name"); + } + + // }}} + // {{{ alterTable() + + /** + * alter an existing table + * + * @param string $name name of the table that is intended to be changed. + * @param array $changes associative array that contains the details of each type + * of change that is intended to be performed. The types of + * changes that are currently supported are defined as follows: + * + * name + * + * New name for the table. + * + * add + * + * Associative array with the names of fields to be added as + * indexes of the array. The value of each entry of the array + * should be set to another associative array with the properties + * of the fields to be added. The properties of the fields should + * be the same as defined by the MDB2 parser. + * + * + * remove + * + * Associative array with the names of fields to be removed as indexes + * of the array. Currently the values assigned to each entry are ignored. + * An empty array should be used for future compatibility. + * + * rename + * + * Associative array with the names of fields to be renamed as indexes + * of the array. The value of each entry of the array should be set to + * another associative array with the entry named name with the new + * field name and the entry named Declaration that is expected to contain + * the portion of the field declaration already in DBMS specific SQL code + * as it is used in the CREATE TABLE statement. + * + * change + * + * Associative array with the names of the fields to be changed as indexes + * of the array. Keep in mind that if it is intended to change either the + * name of a field and any other properties, the change array entries + * should have the new names of the fields as array indexes. + * + * The value of each entry of the array should be set to another associative + * array with the properties of the fields to that are meant to be changed as + * array entries. These entries should be assigned to the new values of the + * respective properties. The properties of the fields should be the same + * as defined by the MDB2 parser. + * + * Example + * array( + * 'name' => 'userlist', + * 'add' => array( + * 'quota' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * ) + * ), + * 'remove' => array( + * 'file_limit' => array(), + * 'time_limit' => array() + * ), + * 'change' => array( + * 'name' => array( + * 'length' => '20', + * 'definition' => array( + * 'type' => 'text', + * 'length' => 20, + * ), + * ) + * ), + * 'rename' => array( + * 'sex' => array( + * 'name' => 'gender', + * 'definition' => array( + * 'type' => 'text', + * 'length' => 1, + * 'default' => 'M', + * ), + * ) + * ) + * ) + * + * @param boolean $check indicates whether the function should just check if the DBMS driver + * can perform the requested table alterations if the value is true or + * actually perform them otherwise. + * @access public + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + */ + function alterTable($name, $changes, $check) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listDatabases() + + /** + * list all databases + * + * @return mixed array of database names on success, a MDB2 error on failure + * @access public + */ + function listDatabases() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implementedd', __FUNCTION__); + } + + // }}} + // {{{ listUsers() + + /** + * list all users + * + * @return mixed array of user names on success, a MDB2 error on failure + * @access public + */ + function listUsers() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listViews() + + /** + * list all views in the current database + * + * @param string database, the current is default + * NB: not all the drivers can get the view names from + * a database other than the current one + * @return mixed array of view names on success, a MDB2 error on failure + * @access public + */ + function listViews($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listTableViews() + + /** + * list the views in the database that reference a given table + * + * @param string table for which all referenced views should be found + * @return mixed array of view names on success, a MDB2 error on failure + * @access public + */ + function listTableViews($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listTableTriggers() + + /** + * list all triggers in the database that reference a given table + * + * @param string table for which all referenced triggers should be found + * @return mixed array of trigger names on success, a MDB2 error on failure + * @access public + */ + function listTableTriggers($table = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listFunctions() + + /** + * list all functions in the current database + * + * @return mixed array of function names on success, a MDB2 error on failure + * @access public + */ + function listFunctions() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listTables() + + /** + * list all tables in the current database + * + * @param string database, the current is default. + * NB: not all the drivers can get the table names from + * a database other than the current one + * @return mixed array of table names on success, a MDB2 error on failure + * @access public + */ + function listTables($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listTableFields() + + /** + * list all fields in a table in the current database + * + * @param string $table name of table that should be used in method + * @return mixed array of field names on success, a MDB2 error on failure + * @access public + */ + function listTableFields($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ createIndex() + + /** + * Get the stucture of a field into an array + * + * @param string $table name of the table on which the index is to be created + * @param string $name name of the index to be created + * @param array $definition associative array that defines properties of the index to be created. + * Currently, only one property named FIELDS is supported. This property + * is also an associative with the names of the index fields as array + * indexes. Each entry of this array is set to another type of associative + * array that specifies properties of the index that are specific to + * each field. + * + * Currently, only the sorting property is supported. It should be used + * to define the sorting direction of the index. It may be set to either + * ascending or descending. + * + * Not all DBMS support index sorting direction configuration. The DBMS + * drivers of those that do not support it ignore this property. Use the + * function supports() to determine whether the DBMS driver can manage indexes. + * + * Example + * array( + * 'fields' => array( + * 'user_name' => array( + * 'sorting' => 'ascending' + * ), + * 'last_login' => array() + * ) + * ) + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createIndex($table, $name, $definition) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $name = $db->quoteIdentifier($db->getIndexName($name), true); + $query = "CREATE INDEX $name ON $table"; + $fields = array(); + foreach (array_keys($definition['fields']) as $field) { + $fields[] = $db->quoteIdentifier($field, true); + } + $query .= ' ('. implode(', ', $fields) . ')'; + return $db->exec($query); + } + + // }}} + // {{{ dropIndex() + + /** + * drop existing index + * + * @param string $table name of table that should be used in method + * @param string $name name of the index to be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropIndex($table, $name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $name = $db->quoteIdentifier($db->getIndexName($name), true); + return $db->exec("DROP INDEX $name"); + } + + // }}} + // {{{ listTableIndexes() + + /** + * list all indexes in a table + * + * @param string $table name of table that should be used in method + * @return mixed array of index names on success, a MDB2 error on failure + * @access public + */ + function listTableIndexes($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ createConstraint() + + /** + * create a constraint on a table + * + * @param string $table name of the table on which the constraint is to be created + * @param string $name name of the constraint to be created + * @param array $definition associative array that defines properties of the constraint to be created. + * Currently, only one property named FIELDS is supported. This property + * is also an associative with the names of the constraint fields as array + * constraints. Each entry of this array is set to another type of associative + * array that specifies properties of the constraint that are specific to + * each field. + * + * Example + * array( + * 'fields' => array( + * 'user_name' => array(), + * 'last_login' => array() + * ) + * ) + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createConstraint($table, $name, $definition) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + $table = $db->quoteIdentifier($table, true); + $name = $db->quoteIdentifier($db->getIndexName($name), true); + $query = "ALTER TABLE $table ADD CONSTRAINT $name"; + if (!empty($definition['primary'])) { + $query.= ' PRIMARY KEY'; + } elseif (!empty($definition['unique'])) { + $query.= ' UNIQUE'; + } + $fields = array(); + foreach (array_keys($definition['fields']) as $field) { + $fields[] = $db->quoteIdentifier($field, true); + } + $query .= ' ('. implode(', ', $fields) . ')'; + return $db->exec($query); + } + + // }}} + // {{{ dropConstraint() + + /** + * drop existing constraint + * + * @param string $table name of table that should be used in method + * @param string $name name of the constraint to be dropped + * @param string $primary hint if the constraint is primary + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropConstraint($table, $name, $primary = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $name = $db->quoteIdentifier($db->getIndexName($name), true); + return $db->exec("ALTER TABLE $table DROP CONSTRAINT $name"); + } + + // }}} + // {{{ listTableConstraints() + + /** + * list all constraints in a table + * + * @param string $table name of table that should be used in method + * @return mixed array of constraint names on success, a MDB2 error on failure + * @access public + */ + function listTableConstraints($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ createSequence() + + /** + * create sequence + * + * @param string $seq_name name of the sequence to be created + * @param string $start start value of the sequence; default is 1 + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createSequence($seq_name, $start = 1) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ dropSequence() + + /** + * drop existing sequence + * + * @param string $seq_name name of the sequence to be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropSequence($name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ listSequences() + + /** + * list all sequences in the current database + * + * @param string database, the current is default + * NB: not all the drivers can get the sequence names from + * a database other than the current one + * @return mixed array of sequence names on success, a MDB2 error on failure + * @access public + */ + function listSequences($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Manager/mysql.php b/MDB2/Driver/Manager/mysql.php new file mode 100644 index 0000000..1e0aa05 --- /dev/null +++ b/MDB2/Driver/Manager/mysql.php @@ -0,0 +1,1001 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.94 2007/03/04 22:50:16 quipo Exp $ +// + +require_once 'MDB2/Driver/Manager/Common.php'; + +/** + * MDB2 MySQL driver for the management modules + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Manager_mysql extends MDB2_Driver_Manager_Common +{ + + // }}} + // {{{ createDatabase() + + /** + * create a new database + * + * @param string $name name of the database that should be created + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createDatabase($name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $name = $db->quoteIdentifier($name, true); + $query = "CREATE DATABASE $name"; + $result = $db->exec($query); + if (PEAR::isError($result)) { + return $result; + } + return MDB2_OK; + } + + // }}} + // {{{ dropDatabase() + + /** + * drop an existing database + * + * @param string $name name of the database that should be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropDatabase($name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $name = $db->quoteIdentifier($name, true); + $query = "DROP DATABASE $name"; + $result = $db->exec($query); + if (PEAR::isError($result)) { + return $result; + } + return MDB2_OK; + } + + // }}} + // {{{ createTable() + + /** + * create a new table + * + * @param string $name Name of the database that should be created + * @param array $fields Associative array that contains the definition of each field of the new table + * The indexes of the array entries are the names of the fields of the table an + * the array entry values are associative arrays like those that are meant to be + * passed with the field definitions to get[Type]Declaration() functions. + * array( + * 'id' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * 'notnull' => 1 + * 'default' => 0 + * ), + * 'name' => array( + * 'type' => 'text', + * 'length' => 12 + * ), + * 'password' => array( + * 'type' => 'text', + * 'length' => 12 + * ) + * ); + * @param array $options An associative array of table options: + * array( + * 'comment' => 'Foo', + * 'charset' => 'utf8', + * 'collate' => 'utf8_unicode_ci', + * 'type' => 'innodb', + * ); + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createTable($name, $fields, $options = array()) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = $this->_getCreateTableQuery($name, $fields, $options); + if (PEAR::isError($query)) { + return $query; + } + + $options_strings = array(); + + if (!empty($options['comment'])) { + $options_strings['comment'] = 'COMMENT = '.$db->quote($options['comment'], 'text'); + } + + if (!empty($options['charset'])) { + $options_strings['charset'] = 'DEFAULT CHARACTER SET '.$options['charset']; + if (!empty($options['collate'])) { + $options_strings['charset'].= ' COLLATE '.$options['collate']; + } + } + + $type = false; + if (!empty($options['type'])) { + $type = $options['type']; + } elseif ($db->options['default_table_type']) { + $type = $db->options['default_table_type']; + } + if ($type) { + $options_strings[] = "ENGINE = $type"; + } + + if (!empty($options_strings)) { + $query .= ' '.implode(' ', $options_strings); + } + return $db->exec($query); + } + + // }}} + // {{{ alterTable() + + /** + * alter an existing table + * + * @param string $name name of the table that is intended to be changed. + * @param array $changes associative array that contains the details of each type + * of change that is intended to be performed. The types of + * changes that are currently supported are defined as follows: + * + * name + * + * New name for the table. + * + * add + * + * Associative array with the names of fields to be added as + * indexes of the array. The value of each entry of the array + * should be set to another associative array with the properties + * of the fields to be added. The properties of the fields should + * be the same as defined by the MDB2 parser. + * + * + * remove + * + * Associative array with the names of fields to be removed as indexes + * of the array. Currently the values assigned to each entry are ignored. + * An empty array should be used for future compatibility. + * + * rename + * + * Associative array with the names of fields to be renamed as indexes + * of the array. The value of each entry of the array should be set to + * another associative array with the entry named name with the new + * field name and the entry named Declaration that is expected to contain + * the portion of the field declaration already in DBMS specific SQL code + * as it is used in the CREATE TABLE statement. + * + * change + * + * Associative array with the names of the fields to be changed as indexes + * of the array. Keep in mind that if it is intended to change either the + * name of a field and any other properties, the change array entries + * should have the new names of the fields as array indexes. + * + * The value of each entry of the array should be set to another associative + * array with the properties of the fields to that are meant to be changed as + * array entries. These entries should be assigned to the new values of the + * respective properties. The properties of the fields should be the same + * as defined by the MDB2 parser. + * + * Example + * array( + * 'name' => 'userlist', + * 'add' => array( + * 'quota' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * ) + * ), + * 'remove' => array( + * 'file_limit' => array(), + * 'time_limit' => array() + * ), + * 'change' => array( + * 'name' => array( + * 'length' => '20', + * 'definition' => array( + * 'type' => 'text', + * 'length' => 20, + * ), + * ) + * ), + * 'rename' => array( + * 'sex' => array( + * 'name' => 'gender', + * 'definition' => array( + * 'type' => 'text', + * 'length' => 1, + * 'default' => 'M', + * ), + * ) + * ) + * ) + * + * @param boolean $check indicates whether the function should just check if the DBMS driver + * can perform the requested table alterations if the value is true or + * actually perform them otherwise. + * @access public + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + */ + function alterTable($name, $changes, $check) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + foreach ($changes as $change_name => $change) { + switch ($change_name) { + case 'add': + case 'remove': + case 'change': + case 'rename': + case 'name': + break; + default: + return $db->raiseError(MDB2_ERROR_CANNOT_ALTER, null, null, + 'change type "'.$change_name.'" not yet supported', __FUNCTION__); + } + } + + if ($check) { + return MDB2_OK; + } + + $query = ''; + if (!empty($changes['name'])) { + $change_name = $db->quoteIdentifier($changes['name'], true); + $query .= 'RENAME TO ' . $change_name; + } + + if (!empty($changes['add']) && is_array($changes['add'])) { + foreach ($changes['add'] as $field_name => $field) { + if ($query) { + $query.= ', '; + } + $query.= 'ADD ' . $db->getDeclaration($field['type'], $field_name, $field); + } + } + + if (!empty($changes['remove']) && is_array($changes['remove'])) { + foreach ($changes['remove'] as $field_name => $field) { + if ($query) { + $query.= ', '; + } + $field_name = $db->quoteIdentifier($field_name, true); + $query.= 'DROP ' . $field_name; + } + } + + $rename = array(); + if (!empty($changes['rename']) && is_array($changes['rename'])) { + foreach ($changes['rename'] as $field_name => $field) { + $rename[$field['name']] = $field_name; + } + } + + if (!empty($changes['change']) && is_array($changes['change'])) { + foreach ($changes['change'] as $field_name => $field) { + if ($query) { + $query.= ', '; + } + if (isset($rename[$field_name])) { + $old_field_name = $rename[$field_name]; + unset($rename[$field_name]); + } else { + $old_field_name = $field_name; + } + $old_field_name = $db->quoteIdentifier($old_field_name, true); + $query.= "CHANGE $old_field_name " . $db->getDeclaration($field['definition']['type'], $field_name, $field['definition']); + } + } + + if (!empty($rename) && is_array($rename)) { + foreach ($rename as $rename_name => $renamed_field) { + if ($query) { + $query.= ', '; + } + $field = $changes['rename'][$renamed_field]; + $renamed_field = $db->quoteIdentifier($renamed_field, true); + $query.= 'CHANGE ' . $renamed_field . ' ' . $db->getDeclaration($field['definition']['type'], $field['name'], $field['definition']); + } + } + + if (!$query) { + return MDB2_OK; + } + + $name = $db->quoteIdentifier($name, true); + return $db->exec("ALTER TABLE $name $query"); + } + + // }}} + // {{{ listDatabases() + + /** + * list all databases + * + * @return mixed array of database names on success, a MDB2 error on failure + * @access public + */ + function listDatabases() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $result = $db->queryCol('SHOW DATABASES'); + if (PEAR::isError($result)) { + return $result; + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ listUsers() + + /** + * list all users + * + * @return mixed array of user names on success, a MDB2 error on failure + * @access public + */ + function listUsers() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->queryCol('SELECT DISTINCT USER FROM mysql.USER'); + } + + // }}} + // {{{ listFunctions() + + /** + * list all functions in the current database + * + * @return mixed array of function names on success, a MDB2 error on failure + * @access public + */ + function listFunctions() + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = "SELECT name FROM mysql.proc"; + /* + SELECT ROUTINE_NAME + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_TYPE = 'FUNCTION' + */ + $result = $db->queryCol($query); + if (PEAR::isError($result)) { + return $result; + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ listTableTriggers() + + /** + * list all triggers in the database that reference a given table + * + * @param string table for which all referenced triggers should be found + * @return mixed array of trigger names on success, a MDB2 error on failure + * @access public + */ + function listTableTriggers($table = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = 'SHOW TRIGGERS'; + if (!is_null($table)) { + $table = $db->quote($table, 'text'); + $query .= " LIKE $table"; + } + $result = $db->queryCol($query); + if (PEAR::isError($result)) { + return $result; + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ listTables() + + /** + * list all tables in the current database + * + * @param string database, the current is default + * @return mixed array of table names on success, a MDB2 error on failure + * @access public + */ + function listTables($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = "SHOW /*!50002 FULL*/ TABLES"; + if (!is_null($database)) { + $query .= " FROM $database"; + } + $query.= "/*!50002 WHERE Table_type = 'BASE TABLE'*/"; + + $table_names = $db->queryAll($query, null, MDB2_FETCHMODE_ORDERED); + if (PEAR::isError($table_names)) { + return $table_names; + } + + $result = array(); + foreach ($table_names as $table) { + if (!$this->_fixSequenceName($table[0], true)) { + $result[] = $table[0]; + } + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ listViews() + + /** + * list all views in the current database + * + * @param string database, the current is default + * @return mixed array of view names on success, a MDB2 error on failure + * @access public + */ + function listViews($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = 'SHOW FULL TABLES'; + if (!is_null($database)) { + $query.= " FROM $database"; + } + $query.= " WHERE Table_type = 'VIEW'"; + + $result = $db->queryCol($query); + if (PEAR::isError($result)) { + return $result; + } + + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ listTableFields() + + /** + * list all fields in a table in the current database + * + * @param string $table name of table that should be used in method + * @return mixed array of field names on success, a MDB2 error on failure + * @access public + */ + function listTableFields($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $result = $db->queryCol("SHOW COLUMNS FROM $table"); + if (PEAR::isError($result)) { + return $result; + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} + // {{{ createIndex() + + /** + * Get the stucture of a field into an array + * + * @author Leoncx + * @param string $table name of the table on which the index is to be created + * @param string $name name of the index to be created + * @param array $definition associative array that defines properties of the index to be created. + * Currently, only one property named FIELDS is supported. This property + * is also an associative with the names of the index fields as array + * indexes. Each entry of this array is set to another type of associative + * array that specifies properties of the index that are specific to + * each field. + * + * Currently, only the sorting property is supported. It should be used + * to define the sorting direction of the index. It may be set to either + * ascending or descending. + * + * Not all DBMS support index sorting direction configuration. The DBMS + * drivers of those that do not support it ignore this property. Use the + * function supports() to determine whether the DBMS driver can manage indexes. + * + * Example + * array( + * 'fields' => array( + * 'user_name' => array( + * 'sorting' => 'ascending' + * 'length' => 10 + * ), + * 'last_login' => array() + * ) + * ) + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createIndex($table, $name, $definition) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $name = $db->quoteIdentifier($db->getIndexName($name), true); + $query = "CREATE INDEX $name ON $table"; + $fields = array(); + foreach ($definition['fields'] as $field => $fieldinfo) { + if (!empty($fieldinfo['length'])) { + $fields[] = $db->quoteIdentifier($field, true) . '(' . $fieldinfo['length'] . ')'; + } else { + $fields[] = $db->quoteIdentifier($field, true); + } + } + $query .= ' ('. implode(', ', $fields) . ')'; + return $db->exec($query); + } + + // }}} + // {{{ dropIndex() + + /** + * drop existing index + * + * @param string $table name of table that should be used in method + * @param string $name name of the index to be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropIndex($table, $name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $name = $db->quoteIdentifier($db->getIndexName($name), true); + return $db->exec("DROP INDEX $name ON $table"); + } + + // }}} + // {{{ listTableIndexes() + + /** + * list all indexes in a table + * + * @param string $table name of table that should be used in method + * @return mixed array of index names on success, a MDB2 error on failure + * @access public + */ + function listTableIndexes($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $key_name = 'Key_name'; + $non_unique = 'Non_unique'; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $key_name = strtolower($key_name); + $non_unique = strtolower($non_unique); + } else { + $key_name = strtoupper($key_name); + $non_unique = strtoupper($non_unique); + } + } + + $table = $db->quoteIdentifier($table, true); + $query = "SHOW INDEX FROM $table"; + $indexes = $db->queryAll($query, null, MDB2_FETCHMODE_ASSOC); + if (PEAR::isError($indexes)) { + return $indexes; + } + + $result = array(); + foreach ($indexes as $index_data) { + if ($index_data[$non_unique] && ($index = $this->_fixIndexName($index_data[$key_name]))) { + $result[$index] = true; + } + } + + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_change_key_case($result, $db->options['field_case']); + } + return array_keys($result); + } + + // }}} + // {{{ createConstraint() + + /** + * create a constraint on a table + * + * @param string $table name of the table on which the constraint is to be created + * @param string $name name of the constraint to be created + * @param array $definition associative array that defines properties of the constraint to be created. + * Currently, only one property named FIELDS is supported. This property + * is also an associative with the names of the constraint fields as array + * constraints. Each entry of this array is set to another type of associative + * array that specifies properties of the constraint that are specific to + * each field. + * + * Example + * array( + * 'fields' => array( + * 'user_name' => array(), + * 'last_login' => array() + * ) + * ) + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createConstraint($table, $name, $definition) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $type = ''; + $name = $db->quoteIdentifier($db->getIndexName($name), true); + if (!empty($definition['primary'])) { + $type = 'PRIMARY'; + $name = 'KEY'; + } elseif (!empty($definition['unique'])) { + $type = 'UNIQUE'; + } + if (empty($type)) { + return $db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'invalid definition, could not create constraint', __FUNCTION__); + } + + $table = $db->quoteIdentifier($table, true); + $query = "ALTER TABLE $table ADD $type $name"; + $fields = array(); + foreach (array_keys($definition['fields']) as $field) { + $fields[] = $db->quoteIdentifier($field, true); + } + $query .= ' ('. implode(', ', $fields) . ')'; + return $db->exec($query); + } + + // }}} + // {{{ dropConstraint() + + /** + * drop existing constraint + * + * @param string $table name of table that should be used in method + * @param string $name name of the constraint to be dropped + * @param string $primary hint if the constraint is primary + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropConstraint($table, $name, $primary = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + if ($primary || strtolower($name) == 'primary') { + $query = "ALTER TABLE $table DROP PRIMARY KEY"; + } else { + $name = $db->quoteIdentifier($db->getIndexName($name), true); + $query = "ALTER TABLE $table DROP INDEX $name"; + } + return $db->exec($query); + } + + // }}} + // {{{ listTableConstraints() + + /** + * list all constraints in a table + * + * @param string $table name of table that should be used in method + * @return mixed array of constraint names on success, a MDB2 error on failure + * @access public + */ + function listTableConstraints($table) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $key_name = 'Key_name'; + $non_unique = 'Non_unique'; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $key_name = strtolower($key_name); + $non_unique = strtolower($non_unique); + } else { + $key_name = strtoupper($key_name); + $non_unique = strtoupper($non_unique); + } + } + + $table = $db->quoteIdentifier($table, true); + $query = "SHOW INDEX FROM $table"; + $indexes = $db->queryAll($query, null, MDB2_FETCHMODE_ASSOC); + if (PEAR::isError($indexes)) { + return $indexes; + } + + $result = array(); + foreach ($indexes as $index_data) { + if (!$index_data[$non_unique]) { + if ($index_data[$key_name] !== 'PRIMARY') { + $index = $this->_fixIndexName($index_data[$key_name]); + } else { + $index = 'PRIMARY'; + } + if (!empty($index)) { + $result[$index] = true; + } + } + } + + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_change_key_case($result, $db->options['field_case']); + } + return array_keys($result); + } + + // }}} + // {{{ createSequence() + + /** + * create sequence + * + * @param string $seq_name name of the sequence to be created + * @param string $start start value of the sequence; default is 1 + * @param array $options An associative array of table options: + * array( + * 'comment' => 'Foo', + * 'charset' => 'utf8', + * 'collate' => 'utf8_unicode_ci', + * 'type' => 'innodb', + * ); + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function createSequence($seq_name, $start = 1, $options = array()) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $sequence_name = $db->quoteIdentifier($db->getSequenceName($seq_name), true); + $seqcol_name = $db->quoteIdentifier($db->options['seqcol_name'], true); + + $options_strings = array(); + + if (!empty($options['comment'])) { + $options_strings['comment'] = 'COMMENT = '.$db->quote($options['comment'], 'text'); + } + + if (!empty($options['charset'])) { + $options_strings['charset'] = 'DEFAULT CHARACTER SET '.$options['charset']; + if (!empty($options['collate'])) { + $options_strings['charset'].= ' COLLATE '.$options['collate']; + } + } + + $type = false; + if (!empty($options['type'])) { + $type = $options['type']; + } elseif ($db->options['default_table_type']) { + $type = $db->options['default_table_type']; + } + if ($type) { + $options_strings[] = "ENGINE = $type"; + } + + $query = "CREATE TABLE $sequence_name ($seqcol_name INT NOT NULL AUTO_INCREMENT, PRIMARY KEY ($seqcol_name))"; + if (!empty($options_strings)) { + $query .= ' '.implode(' ', $options_strings); + } + $res = $db->exec($query); + + if (PEAR::isError($res)) { + return $res; + } + + if ($start == 1) { + return MDB2_OK; + } + + $query = "INSERT INTO $sequence_name ($seqcol_name) VALUES (".($start-1).')'; + $res = $db->exec($query); + if (!PEAR::isError($res)) { + return MDB2_OK; + } + + // Handle error + $result = $db->exec("DROP TABLE $sequence_name"); + if (PEAR::isError($result)) { + return $db->raiseError($result, null, null, + 'could not drop inconsistent sequence table', __FUNCTION__); + } + + return $db->raiseError($res, null, null, + 'could not create sequence table', __FUNCTION__); + } + + // }}} + // {{{ dropSequence() + + /** + * drop existing sequence + * + * @param string $seq_name name of the sequence to be dropped + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function dropSequence($seq_name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $sequence_name = $db->quoteIdentifier($db->getSequenceName($seq_name), true); + return $db->exec("DROP TABLE $sequence_name"); + } + + // }}} + // {{{ listSequences() + + /** + * list all sequences in the current database + * + * @param string database, the current is default + * @return mixed array of sequence names on success, a MDB2 error on failure + * @access public + */ + function listSequences($database = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = "SHOW TABLES"; + if (!is_null($database)) { + $query .= " FROM $database"; + } + $table_names = $db->queryCol($query); + if (PEAR::isError($table_names)) { + return $table_names; + } + + $result = array(); + foreach ($table_names as $table_name) { + if ($sqn = $this->_fixSequenceName($table_name, true)) { + $result[] = $sqn; + } + } + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $result = array_map(($db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'), $result); + } + return $result; + } + + // }}} +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Native/Common.php b/MDB2/Driver/Native/Common.php new file mode 100644 index 0000000..3197d6d --- /dev/null +++ b/MDB2/Driver/Native/Common.php @@ -0,0 +1,58 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.1 2006/06/18 21:59:05 lsmith Exp $ +// + +/** + * Base class for the natuve modules that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Native_Common extends MDB2_Module_Common +{ +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Native/mysql.php b/MDB2/Driver/Native/mysql.php new file mode 100644 index 0000000..90ff068 --- /dev/null +++ b/MDB2/Driver/Native/mysql.php @@ -0,0 +1,60 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.9 2006/06/18 21:59:05 lsmith Exp $ +// + +require_once 'MDB2/Driver/Native/Common.php'; + +/** + * MDB2 MySQL driver for the native module + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Native_mysql extends MDB2_Driver_Native_Common +{ +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Reverse/Common.php b/MDB2/Driver/Reverse/Common.php new file mode 100644 index 0000000..4c0dff3 --- /dev/null +++ b/MDB2/Driver/Reverse/Common.php @@ -0,0 +1,476 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.35 2007/02/25 11:14:34 quipo Exp $ +// + +/** + * @package MDB2 + * @category Database + */ + +/** + * These are constants for the tableInfo-function + * they are bitwised or'ed. so if there are more constants to be defined + * in the future, adjust MDB2_TABLEINFO_FULL accordingly + */ + +define('MDB2_TABLEINFO_ORDER', 1); +define('MDB2_TABLEINFO_ORDERTABLE', 2); +define('MDB2_TABLEINFO_FULL', 3); + +/** + * Base class for the schema reverse engineering module that is extended by each MDB2 driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_Reverse_Common extends MDB2_Module_Common +{ + // }}} + // {{{ getTableFieldDefinition() + + /** + * Get the structure of a field into an array + * + * @param string $table name of table that should be used in method + * @param string $field name of field that should be used in method + * @return mixed data array on success, a MDB2 error on failure. + * The returned array contains an array for each field definition, + * with all or some of these indices, depending on the field data type: + * [notnull] [nativetype] [length] [fixed] [default] [type] [mdb2type] + * @access public + */ + function getTableFieldDefinition($table, $field) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ getTableIndexDefinition() + + /** + * Get the structure of an index into an array + * + * @param string $table name of table that should be used in method + * @param string $index name of index that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * The returned array has this structure: + * + * array ( + * [fields] => array ( + * [field1name] => array() // one entry per each field covered + * [field2name] => array() // by the index + * [field3name] => array( + * [sorting] => ascending + * ) + * ) + * ); + * + * @access public + */ + function getTableIndexDefinition($table, $index) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ getTableConstraintDefinition() + + /** + * Get the structure of an constraints into an array + * + * @param string $table name of table that should be used in method + * @param string $index name of index that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * The returned array has this structure: + *
+     *          array (
+     *              [primary] => 1
+     *              [fields] => array (
+     *                  [field1name] => array() // one entry per each field covered
+     *                  [field2name] => array() // by the index
+     *                  [field3name] => array(
+     *                      [sorting] => ascending
+     *                  )
+     *              )
+     *          );
+     *          
+ * @access public + */ + function getTableConstraintDefinition($table, $index) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ getSequenceDefinition() + + /** + * Get the structure of a sequence into an array + * + * @param string $sequence name of sequence that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * The returned array has this structure: + *
+     *          array (
+     *              [start] => n
+     *          );
+     *          
+ * @access public + */ + function getSequenceDefinition($sequence) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $start = $db->currId($sequence); + if (PEAR::isError($start)) { + return $start; + } + if ($db->supports('current_id')) { + $start++; + } else { + $db->warnings[] = 'database does not support getting current + sequence value, the sequence value was incremented'; + } + $definition = array(); + if ($start != 1) { + $definition = array('start' => $start); + } + return $definition; + } + + // }}} + // {{{ getTriggerDefinition() + + /** + * Get the structure of a trigger into an array + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change the returned value + * at any time until labelled as non-experimental + * + * @param string $trigger name of trigger that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * The returned array has this structure: + *
+     *          array (
+     *              [trigger_name]    => 'trigger name',
+     *              [table_name]      => 'table name',
+     *              [trigger_body]    => 'trigger body definition',
+     *              [trigger_type]    => 'BEFORE' | 'AFTER',
+     *              [trigger_event]   => 'INSERT' | 'UPDATE' | 'DELETE'
+     *                  //or comma separated list of multiple events, when supported
+     *              [trigger_enabled] => true|false
+     *              [trigger_comment] => 'trigger comment',
+     *          );
+     *          
+ * The oci8 driver also returns a [when_clause] index. + * @access public + */ + function getTriggerDefinition($trigger) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * The format of the resulting array depends on which $mode + * you select. The sample output below is based on this query: + *
+     *    SELECT tblFoo.fldID, tblFoo.fldPhone, tblBar.fldId
+     *    FROM tblFoo
+     *    JOIN tblBar ON tblFoo.fldId = tblBar.fldId
+     * 
+ * + *
    + *
  • + * + * null (default) + *
    +     *   [0] => Array (
    +     *       [table] => tblFoo
    +     *       [name] => fldId
    +     *       [type] => int
    +     *       [len] => 11
    +     *       [flags] => primary_key not_null
    +     *   )
    +     *   [1] => Array (
    +     *       [table] => tblFoo
    +     *       [name] => fldPhone
    +     *       [type] => string
    +     *       [len] => 20
    +     *       [flags] =>
    +     *   )
    +     *   [2] => Array (
    +     *       [table] => tblBar
    +     *       [name] => fldId
    +     *       [type] => int
    +     *       [len] => 11
    +     *       [flags] => primary_key not_null
    +     *   )
    +     *   
    + * + *
  • + * + * MDB2_TABLEINFO_ORDER + * + *

    In addition to the information found in the default output, + * a notation of the number of columns is provided by the + * num_fields element while the order + * element provides an array with the column names as the keys and + * their location index number (corresponding to the keys in the + * the default output) as the values.

    + * + *

    If a result set has identical field names, the last one is + * used.

    + * + *
    +     *   [num_fields] => 3
    +     *   [order] => Array (
    +     *       [fldId] => 2
    +     *       [fldTrans] => 1
    +     *   )
    +     *   
    + * + *
  • + * + * MDB2_TABLEINFO_ORDERTABLE + * + *

    Similar to MDB2_TABLEINFO_ORDER but adds more + * dimensions to the array in which the table names are keys and + * the field names are sub-keys. This is helpful for queries that + * join tables which have identical field names.

    + * + *
    +     *   [num_fields] => 3
    +     *   [ordertable] => Array (
    +     *       [tblFoo] => Array (
    +     *           [fldId] => 0
    +     *           [fldPhone] => 1
    +     *       )
    +     *       [tblBar] => Array (
    +     *           [fldId] => 2
    +     *       )
    +     *   )
    +     *   
    + * + *
  • + *
+ * + * The flags element contains a space separated list + * of extra information about the field. This data is inconsistent + * between DBMS's due to the way each DBMS works. + * + primary_key + * + unique_key + * + multiple_key + * + not_null + * + * Most DBMS's only provide the table and flags + * elements if $result is a table name. The following DBMS's + * provide full information from queries: + * + fbsql + * + mysql + * + * If the 'portability' option has MDB2_PORTABILITY_FIX_CASE + * turned on, the names of tables and fields will be lower or upper cased. + * + * @param object|string $result MDB2_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode either unused or one of the tableInfo modes: + * MDB2_TABLEINFO_ORDERTABLE, + * MDB2_TABLEINFO_ORDER or + * MDB2_TABLEINFO_FULL (which does both). + * These are bitwise, so the first two can be + * combined using |. + * + * @return array an associative array with the information requested. + * A MDB2_Error object on failure. + * + * @see MDB2_Driver_Common::setOption() + */ + function tableInfo($result, $mode = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if (!is_string($result)) { + return $db->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'method not implemented', __FUNCTION__); + } + + $db->loadModule('Manager', null, true); + $fields = $db->manager->listTableFields($result); + if (PEAR::isError($fields)) { + return $fields; + } + + $flags = array(); + + $idxname_format = $db->getOption('idxname_format'); + $db->setOption('idxname_format', '%s'); + + $indexes = $db->manager->listTableIndexes($result); + if (PEAR::isError($indexes)) { + $db->setOption('idxname_format', $idxname_format); + return $indexes; + } + + foreach ($indexes as $index) { + $definition = $this->getTableIndexDefinition($result, $index); + if (PEAR::isError($definition)) { + $db->setOption('idxname_format', $idxname_format); + return $definition; + } + if (count($definition['fields']) > 1) { + foreach ($definition['fields'] as $field => $sort) { + $flags[$field] = 'multiple_key'; + } + } + } + + $constraints = $db->manager->listTableConstraints($result); + if (PEAR::isError($constraints)) { + return $constraints; + } + + foreach ($constraints as $constraint) { + $definition = $this->getTableConstraintDefinition($result, $constraint); + if (PEAR::isError($definition)) { + $db->setOption('idxname_format', $idxname_format); + return $definition; + } + $flag = !empty($definition['primary']) + ? 'primary_key' : (!empty($definition['unique']) + ? 'unique_key' : false); + if ($flag) { + foreach ($definition['fields'] as $field => $sort) { + if (empty($flags[$field]) || $flags[$field] != 'primary_key') { + $flags[$field] = $flag; + } + } + } + } + + if ($mode) { + $res['num_fields'] = count($fields); + } + + foreach ($fields as $i => $field) { + $definition = $this->getTableFieldDefinition($result, $field); + if (PEAR::isError($definition)) { + $db->setOption('idxname_format', $idxname_format); + return $definition; + } + $res[$i] = $definition[0]; + $res[$i]['name'] = $field; + $res[$i]['table'] = $result; + $res[$i]['type'] = preg_replace('/^([a-z]+).*$/i', '\\1', trim($definition[0]['nativetype'])); + // 'primary_key', 'unique_key', 'multiple_key' + $res[$i]['flags'] = empty($flags[$field]) ? '' : $flags[$field]; + // not_null', 'unsigned', 'auto_increment', 'default_[rawencodedvalue]' + if (!empty($res[$i]['notnull'])) { + $res[$i]['flags'].= ' not_null'; + } + if (!empty($res[$i]['unsigned'])) { + $res[$i]['flags'].= ' unsigned'; + } + if (!empty($res[$i]['auto_increment'])) { + $res[$i]['flags'].= ' autoincrement'; + } + if (!empty($res[$i]['default'])) { + $res[$i]['flags'].= ' default_'.rawurlencode($res[$i]['default']); + } + + if ($mode & MDB2_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & MDB2_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + $db->setOption('idxname_format', $idxname_format); + return $res; + } +} +?> \ No newline at end of file diff --git a/MDB2/Driver/Reverse/mysql.php b/MDB2/Driver/Reverse/mysql.php new file mode 100644 index 0000000..45a3777 --- /dev/null +++ b/MDB2/Driver/Reverse/mysql.php @@ -0,0 +1,437 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.68 2007/03/29 18:18:06 quipo Exp $ +// + +require_once 'MDB2/Driver/Reverse/Common.php'; + +/** + * MDB2 MySQL driver for the schema reverse engineering module + * + * @package MDB2 + * @category Database + * @author Lukas Smith + * @author Lorenzo Alberton + */ +class MDB2_Driver_Reverse_mysql extends MDB2_Driver_Reverse_Common +{ + // {{{ getTableFieldDefinition() + + /** + * Get the structure of a field into an array + * + * @param string $table name of table that should be used in method + * @param string $field_name name of field that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * @access public + */ + function getTableFieldDefinition($table, $field_name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $result = $db->loadModule('Datatype', null, true); + if (PEAR::isError($result)) { + return $result; + } + $table = $db->quoteIdentifier($table, true); + $query = "SHOW COLUMNS FROM $table LIKE ".$db->quote($field_name); + $columns = $db->queryAll($query, null, MDB2_FETCHMODE_ASSOC); + if (PEAR::isError($columns)) { + return $columns; + } + foreach ($columns as $column) { + $column = array_change_key_case($column, CASE_LOWER); + $column['name'] = $column['field']; + unset($column['field']); + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $column['name'] = strtolower($column['name']); + } else { + $column['name'] = strtoupper($column['name']); + } + } else { + $column = array_change_key_case($column, $db->options['field_case']); + } + if ($field_name == $column['name']) { + $mapped_datatype = $db->datatype->mapNativeDatatype($column); + if (PEAR::IsError($mapped_datatype)) { + return $mapped_datatype; + } + list($types, $length, $unsigned, $fixed) = $mapped_datatype; + $notnull = false; + if (empty($column['null']) || $column['null'] !== 'YES') { + $notnull = true; + } + $default = false; + if (array_key_exists('default', $column)) { + $default = $column['default']; + if (is_null($default) && $notnull) { + $default = ''; + } + } + $autoincrement = false; + if (!empty($column['extra']) && $column['extra'] == 'auto_increment') { + $autoincrement = true; + } + + $definition[0] = array( + 'notnull' => $notnull, + 'nativetype' => preg_replace('/^([a-z]+)[^a-z].*/i', '\\1', $column['type']) + ); + if (!is_null($length)) { + $definition[0]['length'] = $length; + } + if (!is_null($unsigned)) { + $definition[0]['unsigned'] = $unsigned; + } + if (!is_null($fixed)) { + $definition[0]['fixed'] = $fixed; + } + if ($default !== false) { + $definition[0]['default'] = $default; + } + if ($autoincrement !== false) { + $definition[0]['autoincrement'] = $autoincrement; + } + foreach ($types as $key => $type) { + $definition[$key] = $definition[0]; + if ($type == 'clob' || $type == 'blob') { + unset($definition[$key]['default']); + } + $definition[$key]['type'] = $type; + $definition[$key]['mdb2type'] = $type; + } + return $definition; + } + } + + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'it was not specified an existing table column', __FUNCTION__); + } + + // }}} + // {{{ getTableIndexDefinition() + + /** + * Get the structure of an index into an array + * + * @param string $table name of table that should be used in method + * @param string $constraint_name name of constraint that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * @access public + */ + function getTableIndexDefinition($table, $constraint_name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $query = "SHOW INDEX FROM $table /*!50002 WHERE Key_name = %s */"; + $constraint_name_mdb2 = $db->getIndexName($constraint_name); + $result = $db->queryRow(sprintf($query, $db->quote($constraint_name_mdb2))); + if (!PEAR::isError($result) && !is_null($result)) { + // apply 'idxname_format' only if the query succeeded, otherwise + // fallback to the given $index_name, without transformation + $constraint_name = $constraint_name_mdb2; + } + $result = $db->query(sprintf($query, $db->quote($constraint_name))); + if (PEAR::isError($result)) { + return $result; + } + $colpos = 1; + $definition = array(); + while (is_array($row = $result->fetchRow(MDB2_FETCHMODE_ASSOC))) { + $row = array_change_key_case($row, CASE_LOWER); + $key_name = $row['key_name']; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $key_name = strtolower($key_name); + } else { + $key_name = strtoupper($key_name); + } + } + if ($constraint_name == $key_name) { + if (!$row['non_unique']) { + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + $constraint_name . ' is not an existing table constraint', __FUNCTION__); + } + $column_name = $row['column_name']; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $column_name = strtolower($column_name); + } else { + $column_name = strtoupper($column_name); + } + } + $definition['fields'][$column_name] = array( + 'position' => $colpos++ + ); + if (!empty($row['collation'])) { + $definition['fields'][$column_name]['sorting'] = ($row['collation'] == 'A' + ? 'ascending' : 'descending'); + } + } + } + $result->free(); + if (empty($definition['fields'])) { + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + $constraint_name . ' is not an existing table constraint', __FUNCTION__); + } + return $definition; + } + + // }}} + // {{{ getTableConstraintDefinition() + + /** + * Get the structure of a constraint into an array + * + * @param string $table name of table that should be used in method + * @param string $index_name name of index that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * @access public + */ + function getTableConstraintDefinition($table, $index_name) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $table = $db->quoteIdentifier($table, true); + $query = "SHOW INDEX FROM $table /*!50002 WHERE Key_name = %s */"; + if (strtolower($index_name) != 'primary') { + $index_name_mdb2 = $db->getIndexName($index_name); + $result = $db->queryRow(sprintf($query, $db->quote($index_name_mdb2))); + if (!PEAR::isError($result) && !is_null($result)) { + // apply 'idxname_format' only if the query succeeded, otherwise + // fallback to the given $index_name, without transformation + $index_name = $index_name_mdb2; + } + } + $result = $db->query(sprintf($query, $db->quote($index_name))); + if (PEAR::isError($result)) { + return $result; + } + $colpos = 1; + $definition = array(); + while (is_array($row = $result->fetchRow(MDB2_FETCHMODE_ASSOC))) { + $row = array_change_key_case($row, CASE_LOWER); + $key_name = $row['key_name']; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $key_name = strtolower($key_name); + } else { + $key_name = strtoupper($key_name); + } + } + if ($index_name == $key_name) { + if ($row['non_unique']) { + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'it was not specified an existing table constraint', __FUNCTION__); + } + if ($row['key_name'] == 'PRIMARY') { + $definition['primary'] = true; + } else { + $definition['unique'] = true; + } + $column_name = $row['column_name']; + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $column_name = strtolower($column_name); + } else { + $column_name = strtoupper($column_name); + } + } + $definition['fields'][$column_name] = array( + 'position' => $colpos++ + ); + if (!empty($row['collation'])) { + $definition['fields'][$column_name]['sorting'] = ($row['collation'] == 'A' + ? 'ascending' : 'descending'); + } + } + } + $result->free(); + if (empty($definition['fields'])) { + return $db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'it was not specified an existing table constraint', __FUNCTION__); + } + return $definition; + } + + // }}} + // {{{ getTriggerDefinition() + + /** + * Get the structure of a trigger into an array + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change the returned value + * at any time until labelled as non-experimental + * + * @param string $trigger name of trigger that should be used in method + * @return mixed data array on success, a MDB2 error on failure + * @access public + */ + function getTriggerDefinition($trigger) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $query = 'SELECT trigger_name, + event_object_table AS table_name, + action_statement AS trigger_body, + action_timing AS trigger_type, + event_manipulation AS trigger_event + FROM information_schema.triggers + WHERE trigger_name = '. $db->quote($trigger, 'text'); + $types = array( + 'trigger_name' => 'text', + 'table_name' => 'text', + 'trigger_body' => 'text', + 'trigger_type' => 'text', + 'trigger_event' => 'text', + ); + $def = $db->queryRow($query, $types, MDB2_FETCHMODE_ASSOC); + if (PEAR::isError($def)) { + return $def; + } + $def['trigger_comment'] = ''; + $def['trigger_enabled'] = true; + return $def; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result MDB2_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A MDB2_Error object on failure. + * + * @see MDB2_Driver_Common::setOption() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + return parent::tableInfo($result, $mode); + } + + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $resource = MDB2::isResultCommon($result) ? $result->getResource() : $result; + if (!is_resource($resource)) { + return $db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'Could not generate result resource', __FUNCTION__); + } + + if ($db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + if ($db->options['field_case'] == CASE_LOWER) { + $case_func = 'strtolower'; + } else { + $case_func = 'strtoupper'; + } + } else { + $case_func = 'strval'; + } + + $count = @mysql_num_fields($resource); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + $db->loadModule('Datatype', null, true); + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => $case_func(@mysql_field_table($resource, $i)), + 'name' => $case_func(@mysql_field_name($resource, $i)), + 'type' => @mysql_field_type($resource, $i), + 'length' => @mysql_field_len($resource, $i), + 'flags' => @mysql_field_flags($resource, $i), + ); + if ($res[$i]['type'] == 'string') { + $res[$i]['type'] = 'char'; + } elseif ($res[$i]['type'] == 'unknown') { + $res[$i]['type'] = 'decimal'; + } + $mdb2type_info = $db->datatype->mapNativeDatatype($res[$i]); + if (PEAR::isError($mdb2type_info)) { + return $mdb2type_info; + } + $res[$i]['mdb2type'] = $mdb2type_info[0][0]; + if ($mode & MDB2_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & MDB2_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + return $res; + } +} +?> \ No newline at end of file diff --git a/MDB2/Driver/mysql.php b/MDB2/Driver/mysql.php new file mode 100644 index 0000000..baf2ba8 --- /dev/null +++ b/MDB2/Driver/mysql.php @@ -0,0 +1,1479 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mysql.php,v 1.182 2007/05/02 22:00:08 quipo Exp $ +// + +/** + * MDB2 MySQL driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Driver_mysql extends MDB2_Driver_Common +{ + // {{{ properties + var $string_quoting = array('start' => "'", 'end' => "'", 'escape' => '\\', 'escape_pattern' => '\\'); + + var $identifier_quoting = array('start' => '`', 'end' => '`', 'escape' => '`'); + + var $sql_comments = array( + array('start' => '-- ', 'end' => "\n", 'escape' => false), + array('start' => '#', 'end' => "\n", 'escape' => false), + array('start' => '/*', 'end' => '*/', 'escape' => false), + ); + + var $start_transaction = false; + + var $varchar_max_length = 255; + // }}} + // {{{ constructor + + /** + * Constructor + */ + function __construct() + { + parent::__construct(); + + $this->phptype = 'mysql'; + $this->dbsyntax = 'mysql'; + + $this->supported['sequences'] = 'emulated'; + $this->supported['indexes'] = true; + $this->supported['affected_rows'] = true; + $this->supported['transactions'] = false; + $this->supported['savepoints'] = false; + $this->supported['summary_functions'] = true; + $this->supported['order_by_text'] = true; + $this->supported['current_id'] = 'emulated'; + $this->supported['limit_queries'] = true; + $this->supported['LOBs'] = true; + $this->supported['replace'] = true; + $this->supported['sub_selects'] = 'emulated'; + $this->supported['auto_increment'] = true; + $this->supported['primary_key'] = true; + $this->supported['result_introspection'] = true; + $this->supported['prepared_statements'] = 'emulated'; + $this->supported['identifier_quoting'] = true; + $this->supported['pattern_escaping'] = true; + $this->supported['new_link'] = true; + + $this->options['default_table_type'] = ''; + } + + // }}} + // {{{ errorInfo() + + /** + * This method is used to collect information about an error + * + * @param integer $error + * @return array + * @access public + */ + function errorInfo($error = null) + { + if ($this->connection) { + $native_code = @mysql_errno($this->connection); + $native_msg = @mysql_error($this->connection); + } else { + $native_code = @mysql_errno(); + $native_msg = @mysql_error(); + } + if (is_null($error)) { + static $ecode_map; + if (empty($ecode_map)) { + $ecode_map = array( + 1004 => MDB2_ERROR_CANNOT_CREATE, + 1005 => MDB2_ERROR_CANNOT_CREATE, + 1006 => MDB2_ERROR_CANNOT_CREATE, + 1007 => MDB2_ERROR_ALREADY_EXISTS, + 1008 => MDB2_ERROR_CANNOT_DROP, + 1022 => MDB2_ERROR_ALREADY_EXISTS, + 1044 => MDB2_ERROR_ACCESS_VIOLATION, + 1046 => MDB2_ERROR_NODBSELECTED, + 1048 => MDB2_ERROR_CONSTRAINT, + 1049 => MDB2_ERROR_NOSUCHDB, + 1050 => MDB2_ERROR_ALREADY_EXISTS, + 1051 => MDB2_ERROR_NOSUCHTABLE, + 1054 => MDB2_ERROR_NOSUCHFIELD, + 1061 => MDB2_ERROR_ALREADY_EXISTS, + 1062 => MDB2_ERROR_ALREADY_EXISTS, + 1064 => MDB2_ERROR_SYNTAX, + 1091 => MDB2_ERROR_NOT_FOUND, + 1100 => MDB2_ERROR_NOT_LOCKED, + 1136 => MDB2_ERROR_VALUE_COUNT_ON_ROW, + 1142 => MDB2_ERROR_ACCESS_VIOLATION, + 1146 => MDB2_ERROR_NOSUCHTABLE, + 1216 => MDB2_ERROR_CONSTRAINT, + 1217 => MDB2_ERROR_CONSTRAINT, + 1356 => MDB2_ERROR_DIVZERO, + 1451 => MDB2_ERROR_CONSTRAINT, + 1452 => MDB2_ERROR_CONSTRAINT, + ); + } + if ($this->options['portability'] & MDB2_PORTABILITY_ERRORS) { + $ecode_map[1022] = MDB2_ERROR_CONSTRAINT; + $ecode_map[1048] = MDB2_ERROR_CONSTRAINT_NOT_NULL; + $ecode_map[1062] = MDB2_ERROR_CONSTRAINT; + } else { + // Doing this in case mode changes during runtime. + $ecode_map[1022] = MDB2_ERROR_ALREADY_EXISTS; + $ecode_map[1048] = MDB2_ERROR_CONSTRAINT; + $ecode_map[1062] = MDB2_ERROR_ALREADY_EXISTS; + } + if (isset($ecode_map[$native_code])) { + $error = $ecode_map[$native_code]; + } + } + return array($error, $native_code, $native_msg); + } + + // }}} + // {{{ escape() + + /** + * Quotes a string so it can be safely used in a query. It will quote + * the text so it can safely be used within a query. + * + * @param string the input string to quote + * @param bool escape wildcards + * + * @return string quoted string + * + * @access public + */ + function escape($text, $escape_wildcards = false) + { + if ($escape_wildcards) { + $text = $this->escapePattern($text); + } + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + $text = @mysql_real_escape_string($text, $connection); + return $text; + } + + // }}} + // {{{ + + /** + * Start a transaction or set a savepoint. + * + * @param string name of a savepoint to set + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function beginTransaction($savepoint = null) + { + $this->debug('Starting transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + $this->_getServerCapabilities(); + if (!is_null($savepoint)) { + if (!$this->supports('savepoints')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'savepoints are not supported', __FUNCTION__); + } + if (!$this->in_transaction) { + return $this->raiseError(MDB2_ERROR_INVALID, null, null, + 'savepoint cannot be released when changes are auto committed', __FUNCTION__); + } + $query = 'SAVEPOINT '.$savepoint; + return $this->_doQuery($query, true); + } elseif ($this->in_transaction) { + return MDB2_OK; //nothing to do + } + if (!$this->destructor_registered && $this->opened_persistent) { + $this->destructor_registered = true; + register_shutdown_function('MDB2_closeOpenTransactions'); + } + $query = $this->start_transaction ? 'START TRANSACTION' : 'SET AUTOCOMMIT = 1'; + $result =& $this->_doQuery($query, true); + if (PEAR::isError($result)) { + return $result; + } + $this->in_transaction = true; + return MDB2_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the database changes done during a transaction that is in + * progress or release a savepoint. This function may only be called when + * auto-committing is disabled, otherwise it will fail. Therefore, a new + * transaction is implicitly started after committing the pending changes. + * + * @param string name of a savepoint to release + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function commit($savepoint = null) + { + $this->debug('Committing transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + if (!$this->in_transaction) { + return $this->raiseError(MDB2_ERROR_INVALID, null, null, + 'commit/release savepoint cannot be done changes are auto committed', __FUNCTION__); + } + if (!is_null($savepoint)) { + if (!$this->supports('savepoints')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'savepoints are not supported', __FUNCTION__); + } + $server_info = $this->getServerVersion(); + if (version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '5.0.3', '<')) { + return MDB2_OK; + } + $query = 'RELEASE SAVEPOINT '.$savepoint; + return $this->_doQuery($query, true); + } + + if (!$this->supports('transactions')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'transactions are not supported', __FUNCTION__); + } + + $result =& $this->_doQuery('COMMIT', true); + if (PEAR::isError($result)) { + return $result; + } + if (!$this->start_transaction) { + $query = 'SET AUTOCOMMIT = 0'; + $result =& $this->_doQuery($query, true); + if (PEAR::isError($result)) { + return $result; + } + } + $this->in_transaction = false; + return MDB2_OK; + } + + // }}} + // {{{ rollback() + + /** + * Cancel any database changes done during a transaction or since a specific + * savepoint that is in progress. This function may only be called when + * auto-committing is disabled, otherwise it will fail. Therefore, a new + * transaction is implicitly started after canceling the pending changes. + * + * @param string name of a savepoint to rollback to + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + */ + function rollback($savepoint = null) + { + $this->debug('Rolling back transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint)); + if (!$this->in_transaction) { + return $this->raiseError(MDB2_ERROR_INVALID, null, null, + 'rollback cannot be done changes are auto committed', __FUNCTION__); + } + if (!is_null($savepoint)) { + if (!$this->supports('savepoints')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'savepoints are not supported', __FUNCTION__); + } + $query = 'ROLLBACK TO SAVEPOINT '.$savepoint; + return $this->_doQuery($query, true); + } + + $query = 'ROLLBACK'; + $result =& $this->_doQuery($query, true); + if (PEAR::isError($result)) { + return $result; + } + if (!$this->start_transaction) { + $query = 'SET AUTOCOMMIT = 0'; + $result =& $this->_doQuery($query, true); + if (PEAR::isError($result)) { + return $result; + } + } + $this->in_transaction = false; + return MDB2_OK; + } + + // }}} + // {{{ function setTransactionIsolation() + + /** + * Set the transacton isolation level. + * + * @param string standard isolation level + * READ UNCOMMITTED (allows dirty reads) + * READ COMMITTED (prevents dirty reads) + * REPEATABLE READ (prevents nonrepeatable reads) + * SERIALIZABLE (prevents phantom reads) + * @return mixed MDB2_OK on success, a MDB2 error on failure + * + * @access public + * @since 2.1.1 + */ + function setTransactionIsolation($isolation) + { + $this->debug('Setting transaction isolation level', __FUNCTION__, array('is_manip' => true)); + if (!$this->supports('transactions')) { + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'transactions are not supported', __FUNCTION__); + } + switch ($isolation) { + case 'READ UNCOMMITTED': + case 'READ COMMITTED': + case 'REPEATABLE READ': + case 'SERIALIZABLE': + break; + default: + return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null, + 'isolation level is not supported: '.$isolation, __FUNCTION__); + } + + $query = "SET SESSION TRANSACTION ISOLATION LEVEL $isolation"; + return $this->_doQuery($query, true); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database + * + * @return true on success, MDB2 Error Object on failure + */ + function connect() + { + if (is_resource($this->connection)) { + if (count(array_diff($this->connected_dsn, $this->dsn)) == 0 + && $this->opened_persistent == $this->options['persistent'] + && $this->connected_database_name == $this->database_name + ) { + return MDB2_OK; + } + $this->disconnect(false); + } + + if (!PEAR::loadExtension($this->phptype)) { + return $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'extension '.$this->phptype.' is not compiled into PHP', __FUNCTION__); + } + + $params = array(); + if ($this->dsn['protocol'] && $this->dsn['protocol'] == 'unix') { + $params[0] = ':' . $this->dsn['socket']; + } else { + $params[0] = $this->dsn['hostspec'] ? $this->dsn['hostspec'] + : 'localhost'; + if ($this->dsn['port']) { + $params[0].= ':' . $this->dsn['port']; + } + } + $params[] = $this->dsn['username'] ? $this->dsn['username'] : null; + $params[] = $this->dsn['password'] ? $this->dsn['password'] : null; + if (!$this->options['persistent']) { + if (isset($this->dsn['new_link']) + && ($this->dsn['new_link'] == 'true' || $this->dsn['new_link'] === true) + ) { + $params[] = true; + } else { + $params[] = false; + } + } + if (version_compare(phpversion(), '4.3.0', '>=')) { + $params[] = isset($this->dsn['client_flags']) + ? $this->dsn['client_flags'] : null; + } + $connect_function = $this->options['persistent'] ? 'mysql_pconnect' : 'mysql_connect'; + + $connection = @call_user_func_array($connect_function, $params); + if (!$connection) { + if (($err = @mysql_error()) != '') { + return $this->raiseError(MDB2_ERROR_CONNECT_FAILED, null, null, + $err, __FUNCTION__); + } else { + return $this->raiseError(MDB2_ERROR_CONNECT_FAILED, null, null, + 'unable to establish a connection', __FUNCTION__); + } + } + + if (!empty($this->dsn['charset'])) { + $result = $this->setCharset($this->dsn['charset'], $connection); + if (PEAR::isError($result)) { + return $result; + } + } + + $this->connection = $connection; + $this->connected_dsn = $this->dsn; + $this->connected_database_name = ''; + $this->opened_persistent = $this->options['persistent']; + $this->dbsyntax = $this->dsn['dbsyntax'] ? $this->dsn['dbsyntax'] : $this->phptype; + + if ($this->database_name) { + if ($this->database_name != $this->connected_database_name) { + if (!@mysql_select_db($this->database_name, $connection)) { + $err = $this->raiseError(null, null, null, + 'Could not select the database: '.$this->database_name, __FUNCTION__); + return $err; + } + $this->connected_database_name = $this->database_name; + } + } + + $this->supported['transactions'] = $this->options['use_transactions']; + if ($this->options['default_table_type']) { + switch (strtoupper($this->options['default_table_type'])) { + case 'BLACKHOLE': + case 'MEMORY': + case 'ARCHIVE': + case 'CSV': + case 'HEAP': + case 'ISAM': + case 'MERGE': + case 'MRG_ISAM': + case 'ISAM': + case 'MRG_MYISAM': + case 'MYISAM': + $this->supported['transactions'] = false; + $this->warnings[] = $this->options['default_table_type'] . + ' is not a supported default table type'; + break; + } + } + + $this->_getServerCapabilities(); + + return MDB2_OK; + } + + // }}} + // {{{ setCharset() + + /** + * Set the charset on the current connection + * + * @param string charset + * @param resource connection handle + * + * @return true on success, MDB2 Error Object on failure + */ + function setCharset($charset, $connection = null) + { + if (is_null($connection)) { + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + } + $query = "SET NAMES '".mysql_real_escape_string($charset, $connection)."'"; + return $this->_doQuery($query, true, $connection); + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @param boolean $force if the disconnect should be forced even if the + * connection is opened persistently + * @return mixed true on success, false if not connected and error + * object on error + * @access public + */ + function disconnect($force = true) + { + if (is_resource($this->connection)) { + if ($this->in_transaction) { + $dsn = $this->dsn; + $database_name = $this->database_name; + $persistent = $this->options['persistent']; + $this->dsn = $this->connected_dsn; + $this->database_name = $this->connected_database_name; + $this->options['persistent'] = $this->opened_persistent; + $this->rollback(); + $this->dsn = $dsn; + $this->database_name = $database_name; + $this->options['persistent'] = $persistent; + } + + if (!$this->opened_persistent || $force) { + @mysql_close($this->connection); + } + } + return parent::disconnect($force); + } + + // }}} + // {{{ _doQuery() + + /** + * Execute a query + * @param string $query query + * @param boolean $is_manip if the query is a manipulation query + * @param resource $connection + * @param string $database_name + * @return result or error object + * @access protected + */ + function &_doQuery($query, $is_manip = false, $connection = null, $database_name = null) + { + $this->last_query = $query; + $result = $this->debug($query, 'query', array('is_manip' => $is_manip, 'when' => 'pre')); + if ($result) { + if (PEAR::isError($result)) { + return $result; + } + $query = $result; + } + if ($this->options['disable_query']) { + $result = $is_manip ? 0 : null; + return $result; + } + + if (is_null($connection)) { + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + } + if (is_null($database_name)) { + $database_name = $this->database_name; + } + + if ($database_name) { + if ($database_name != $this->connected_database_name) { + if (!@mysql_select_db($database_name, $connection)) { + $err = $this->raiseError(null, null, null, + 'Could not select the database: '.$database_name, __FUNCTION__); + return $err; + } + $this->connected_database_name = $database_name; + } + } + + $function = $this->options['result_buffering'] + ? 'mysql_query' : 'mysql_unbuffered_query'; + $result = @$function($query, $connection); + if (!$result) { + $err =& $this->raiseError(null, null, null, + 'Could not execute statement', __FUNCTION__); + return $err; + } + + $this->debug($query, 'query', array('is_manip' => $is_manip, 'when' => 'post', 'result' => $result)); + return $result; + } + + // }}} + // {{{ _affectedRows() + + /** + * Returns the number of rows affected + * + * @param resource $result + * @param resource $connection + * @return mixed MDB2 Error Object or the number of rows affected + * @access private + */ + function _affectedRows($connection, $result = null) + { + if (is_null($connection)) { + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + } + return @mysql_affected_rows($connection); + } + + // }}} + // {{{ _modifyQuery() + + /** + * Changes a query string for various DBMS specific reasons + * + * @param string $query query to modify + * @param boolean $is_manip if it is a DML query + * @param integer $limit limit the number of rows + * @param integer $offset start reading from given offset + * @return string modified query + * @access protected + */ + function _modifyQuery($query, $is_manip, $limit, $offset) + { + if ($this->options['portability'] & MDB2_PORTABILITY_DELETE_COUNT) { + // "DELETE FROM table" gives 0 affected rows in MySQL. + // This little hack lets you know how many rows were deleted. + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + if ($limit > 0 + && !preg_match('/LIMIT\s*\d(?:\s*(?:,|OFFSET)\s*\d+)?(?:[^\)]*)?$/i', $query) + ) { + $query = rtrim($query); + if (substr($query, -1) == ';') { + $query = substr($query, 0, -1); + } + + // LIMIT doesn't always come last in the query + // @see http://dev.mysql.com/doc/refman/5.0/en/select.html + $after = ''; + if (preg_match('/(\s+INTO\s+(?:OUT|DUMP)FILE\s.*)$/ims', $query, $matches)) { + $after = $matches[0]; + $query = preg_replace('/(\s+INTO\s+(?:OUT|DUMP)FILE\s.*)$/ims', '', $query); + } elseif (preg_match('/(\s+FOR\s+UPDATE\s*)$/i', $query, $matches)) { + $after = $matches[0]; + $query = preg_replace('/(\s+FOR\s+UPDATE\s*)$/im', '', $query); + } elseif (preg_match('/(\s+LOCK\s+IN\s+SHARE\s+MODE\s*)$/im', $query, $matches)) { + $after = $matches[0]; + $query = preg_replace('/(\s+LOCK\s+IN\s+SHARE\s+MODE\s*)$/im', '', $query); + } + + if ($is_manip) { + return $query . " LIMIT $limit" . $after; + } else { + return $query . " LIMIT $offset, $limit" . $after; + } + } + return $query; + } + + // }}} + // {{{ getServerVersion() + + /** + * return version information about the server + * + * @param bool $native determines if the raw version string should be returned + * @return mixed array/string with version information or MDB2 error object + * @access public + */ + function getServerVersion($native = false) + { + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + if ($this->connected_server_info) { + $server_info = $this->connected_server_info; + } else { + $server_info = @mysql_get_server_info($connection); + } + if (!$server_info) { + return $this->raiseError(null, null, null, + 'Could not get server information', __FUNCTION__); + } + // cache server_info + $this->connected_server_info = $server_info; + if (!$native) { + $tmp = explode('.', $server_info, 3); + if (isset($tmp[2]) && strpos($tmp[2], '-')) { + $tmp2 = explode('-', @$tmp[2], 2); + } else { + $tmp2[0] = isset($tmp[2]) ? $tmp[2] : null; + $tmp2[1] = null; + } + $server_info = array( + 'major' => isset($tmp[0]) ? $tmp[0] : null, + 'minor' => isset($tmp[1]) ? $tmp[1] : null, + 'patch' => $tmp2[0], + 'extra' => $tmp2[1], + 'native' => $server_info, + ); + } + return $server_info; + } + + // }}} + // {{{ _getServerCapabilities() + + /** + * Fetch some information about the server capabilities + * (transactions, subselects, prepared statements, etc). + * + * @access private + */ + function _getServerCapabilities() + { + static $already_checked = false; + if (!$already_checked) { + $already_checked = true; + + //set defaults + $this->supported['sub_selects'] = 'emulated'; + $this->supported['prepared_statements'] = 'emulated'; + $this->start_transaction = false; + $this->varchar_max_length = 255; + + $server_info = $this->getServerVersion(); + if (is_array($server_info)) { + if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.1.0', '<')) { + $this->supported['sub_selects'] = true; + $this->supported['prepared_statements'] = true; + } + + if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.0.14', '<') + || !version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.1.1', '<') + ) { + $this->supported['savepoints'] = true; + } + + if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.0.11', '<')) { + $this->start_transaction = true; + } + + if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '5.0.3', '<')) { + $this->varchar_max_length = 65532; + } + } + } + } + + // }}} + // {{{ function _skipUserDefinedVariable($query, $position) + + /** + * Utility method, used by prepare() to avoid misinterpreting MySQL user + * defined variables (SELECT @x:=5) for placeholders. + * Check if the placeholder is a false positive, i.e. if it is an user defined + * variable instead. If so, skip it and advance the position, otherwise + * return the current position, which is valid + * + * @param string $query + * @param integer $position current string cursor position + * @return integer $new_position + * @access protected + */ + function _skipUserDefinedVariable($query, $position) + { + $found = strpos(strrev(substr($query, 0, $position)), '@'); + if ($found === false) { + return $position; + } + $pos = strlen($query) - strlen(substr($query, $position)) - $found - 1; + $substring = substr($query, $pos, $position - $pos + 2); + if (preg_match('/^@\w+:=$/', $substring)) { + return $position + 1; //found an user defined variable: skip it + } + return $position; + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute(). + * With some database backends, this is emulated. + * prepare() requires a generic query as string like + * 'INSERT INTO numbers VALUES(?,?)' or + * 'INSERT INTO numbers VALUES(:foo,:bar)'. + * The ? and :[a-zA-Z] and are placeholders which can be set using + * bindParam() and the query can be send off using the execute() method. + * + * @param string $query the query to prepare + * @param mixed $types array that contains the types of the placeholders + * @param mixed $result_types array that contains the types of the columns in + * the result set or MDB2_PREPARE_RESULT, if set to + * MDB2_PREPARE_MANIP the query is handled as a manipulation query + * @param mixed $lobs key (field) value (parameter) pair for all lob placeholders + * @return mixed resource handle for the prepared query on success, a MDB2 + * error on failure + * @access public + * @see bindParam, execute + */ + function &prepare($query, $types = null, $result_types = null, $lobs = array()) + { + if ($this->options['emulate_prepared'] + || $this->supported['prepared_statements'] !== true + ) { + $obj =& parent::prepare($query, $types, $result_types, $lobs); + return $obj; + } + $is_manip = ($result_types === MDB2_PREPARE_MANIP); + $offset = $this->offset; + $limit = $this->limit; + $this->offset = $this->limit = 0; + $query = $this->_modifyQuery($query, $is_manip, $limit, $offset); + $result = $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'pre')); + if ($result) { + if (PEAR::isError($result)) { + return $result; + } + $query = $result; + } + $placeholder_type_guess = $placeholder_type = null; + $question = '?'; + $colon = ':'; + $positions = array(); + $position = 0; + while ($position < strlen($query)) { + $q_position = strpos($query, $question, $position); + $c_position = strpos($query, $colon, $position); + if ($q_position && $c_position) { + $p_position = min($q_position, $c_position); + } elseif ($q_position) { + $p_position = $q_position; + } elseif ($c_position) { + $p_position = $c_position; + } else { + break; + } + if (is_null($placeholder_type)) { + $placeholder_type_guess = $query[$p_position]; + } + + $new_pos = $this->_skipDelimitedStrings($query, $position, $p_position); + if (PEAR::isError($new_pos)) { + return $new_pos; + } + if ($new_pos != $position) { + $position = $new_pos; + continue; //evaluate again starting from the new position + } + + if ($query[$position] == $placeholder_type_guess) { + if (is_null($placeholder_type)) { + $placeholder_type = $query[$p_position]; + $question = $colon = $placeholder_type; + } + if ($placeholder_type == ':') { + //make sure this is not part of an user defined variable + $new_pos = $this->_skipUserDefinedVariable($query, $position); + if ($new_pos != $position) { + $position = $new_pos; + continue; //evaluate again starting from the new position + } + $parameter = preg_replace('/^.{'.($position+1).'}([a-z0-9_]+).*$/si', '\\1', $query); + if ($parameter === '') { + $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'named parameter with an empty name', __FUNCTION__); + return $err; + } + $positions[$p_position] = $parameter; + $query = substr_replace($query, '?', $position, strlen($parameter)+1); + } else { + $positions[$p_position] = count($positions); + } + $position = $p_position + 1; + } else { + $position = $p_position; + } + } + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + $statement_name = sprintf($this->options['statement_format'], $this->phptype, md5(time() + rand())); + $query = "PREPARE $statement_name FROM ".$this->quote($query, 'text'); + $statement =& $this->_doQuery($query, true, $connection); + if (PEAR::isError($statement)) { + return $statement; + } + + $class_name = 'MDB2_Statement_'.$this->phptype; + $obj =& new $class_name($this, $statement_name, $positions, $query, $types, $result_types, $is_manip, $limit, $offset); + $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'post', 'result' => $obj)); + return $obj; + } + + // }}} + // {{{ replace() + + /** + * Execute a SQL REPLACE query. A REPLACE query is identical to a INSERT + * query, except that if there is already a row in the table with the same + * key field values, the REPLACE query just updates its values instead of + * inserting a new row. + * + * The REPLACE type of query does not make part of the SQL standards. Since + * practically only MySQL implements it natively, this type of query is + * emulated through this method for other DBMS using standard types of + * queries inside a transaction to assure the atomicity of the operation. + * + * @access public + * + * @param string $table name of the table on which the REPLACE query will + * be executed. + * @param array $fields associative array that describes the fields and the + * values that will be inserted or updated in the specified table. The + * indexes of the array are the names of all the fields of the table. The + * values of the array are also associative arrays that describe the + * values and other properties of the table fields. + * + * Here follows a list of field properties that need to be specified: + * + * value: + * Value to be assigned to the specified field. This value may be + * of specified in database independent type format as this + * function can perform the necessary datatype conversions. + * + * Default: + * this property is required unless the Null property + * is set to 1. + * + * type + * Name of the type of the field. Currently, all types Metabase + * are supported except for clob and blob. + * + * Default: no type conversion + * + * null + * Boolean property that indicates that the value for this field + * should be set to null. + * + * The default value for fields missing in INSERT queries may be + * specified the definition of a table. Often, the default value + * is already null, but since the REPLACE may be emulated using + * an UPDATE query, make sure that all fields of the table are + * listed in this function argument array. + * + * Default: 0 + * + * key + * Boolean property that indicates that this field should be + * handled as a primary key or at least as part of the compound + * unique index of the table that will determine the row that will + * updated if it exists or inserted a new row otherwise. + * + * This function will fail if no key field is specified or if the + * value of a key field is set to null because fields that are + * part of unique index they may not be null. + * + * Default: 0 + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + */ + function replace($table, $fields) + { + $count = count($fields); + $query = $values = ''; + $keys = $colnum = 0; + for (reset($fields); $colnum < $count; next($fields), $colnum++) { + $name = key($fields); + if ($colnum > 0) { + $query .= ','; + $values.= ','; + } + $query.= $name; + if (isset($fields[$name]['null']) && $fields[$name]['null']) { + $value = 'NULL'; + } else { + $type = isset($fields[$name]['type']) ? $fields[$name]['type'] : null; + $value = $this->quote($fields[$name]['value'], $type); + } + $values.= $value; + if (isset($fields[$name]['key']) && $fields[$name]['key']) { + if ($value === 'NULL') { + return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null, + 'key value '.$name.' may not be NULL', __FUNCTION__); + } + $keys++; + } + } + if ($keys == 0) { + return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null, + 'not specified which fields are keys', __FUNCTION__); + } + + $connection = $this->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $query = "REPLACE INTO $table ($query) VALUES ($values)"; + $result =& $this->_doQuery($query, true, $connection); + if (PEAR::isError($result)) { + return $result; + } + return $this->_affectedRows($connection, $result); + } + + // }}} + // {{{ nextID() + + /** + * Returns the next free id of a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true the sequence is + * automatic created, if it + * not exists + * + * @return mixed MDB2 Error Object or id + * @access public + */ + function nextID($seq_name, $ondemand = true) + { + $sequence_name = $this->quoteIdentifier($this->getSequenceName($seq_name), true); + $seqcol_name = $this->quoteIdentifier($this->options['seqcol_name'], true); + $query = "INSERT INTO $sequence_name ($seqcol_name) VALUES (NULL)"; + $this->expectError(MDB2_ERROR_NOSUCHTABLE); + $result =& $this->_doQuery($query, true); + $this->popExpect(); + if (PEAR::isError($result)) { + if ($ondemand && $result->getCode() == MDB2_ERROR_NOSUCHTABLE) { + $this->loadModule('Manager', null, true); + $result = $this->manager->createSequence($seq_name); + if (PEAR::isError($result)) { + return $this->raiseError($result, null, null, + 'on demand sequence '.$seq_name.' could not be created', __FUNCTION__); + } else { + return $this->nextID($seq_name, false); + } + } + return $result; + } + $value = $this->lastInsertID(); + if (is_numeric($value)) { + $query = "DELETE FROM $sequence_name WHERE $seqcol_name < $value"; + $result =& $this->_doQuery($query, true); + if (PEAR::isError($result)) { + $this->warnings[] = 'nextID: could not delete previous sequence table values from '.$seq_name; + } + } + return $value; + } + + // }}} + // {{{ lastInsertID() + + /** + * Returns the autoincrement ID if supported or $id or fetches the current + * ID in a sequence called: $table.(empty($field) ? '' : '_'.$field) + * + * @param string $table name of the table into which a new row was inserted + * @param string $field name of the field into which a new row was inserted + * @return mixed MDB2 Error Object or id + * @access public + */ + function lastInsertID($table = null, $field = null) + { + // not using mysql_insert_id() due to http://pear.php.net/bugs/bug.php?id=8051 + return $this->queryOne('SELECT LAST_INSERT_ID()', 'integer'); + } + + // }}} + // {{{ currID() + + /** + * Returns the current id of a sequence + * + * @param string $seq_name name of the sequence + * @return mixed MDB2 Error Object or id + * @access public + */ + function currID($seq_name) + { + $sequence_name = $this->quoteIdentifier($this->getSequenceName($seq_name), true); + $seqcol_name = $this->quoteIdentifier($this->options['seqcol_name'], true); + $query = "SELECT MAX($seqcol_name) FROM $sequence_name"; + return $this->queryOne($query, 'integer'); + } +} + +/** + * MDB2 MySQL result driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Result_mysql extends MDB2_Result_Common +{ + // }}} + // {{{ fetchRow() + + /** + * Fetch a row and insert the data into an existing array. + * + * @param int $fetchmode how the array data should be indexed + * @param int $rownum number of the row where the data can be found + * @return int data array on success, a MDB2 error on failure + * @access public + */ + function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null) + { + if (!is_null($rownum)) { + $seek = $this->seek($rownum); + if (PEAR::isError($seek)) { + return $seek; + } + } + if ($fetchmode == MDB2_FETCHMODE_DEFAULT) { + $fetchmode = $this->db->fetchmode; + } + if ($fetchmode & MDB2_FETCHMODE_ASSOC) { + $row = @mysql_fetch_assoc($this->result); + if (is_array($row) + && $this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE + ) { + $row = array_change_key_case($row, $this->db->options['field_case']); + } + } else { + $row = @mysql_fetch_row($this->result); + } + + if (!$row) { + if ($this->result === false) { + $err =& $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'resultset has already been freed', __FUNCTION__); + return $err; + } + $null = null; + return $null; + } + $mode = $this->db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL; + if ($mode) { + $this->db->_fixResultArrayValues($row, $mode); + } + if (!empty($this->types)) { + $row = $this->db->datatype->convertResultRow($this->types, $row, false); + } + if (!empty($this->values)) { + $this->_assignBindColumns($row); + } + if ($fetchmode === MDB2_FETCHMODE_OBJECT) { + $object_class = $this->db->options['fetch_class']; + if ($object_class == 'stdClass') { + $row = (object) $row; + } else { + $row = &new $object_class($row); + } + } + ++$this->rownum; + return $row; + } + + // }}} + // {{{ _getColumnNames() + + /** + * Retrieve the names of columns returned by the DBMS in a query result. + * + * @return mixed Array variable that holds the names of columns as keys + * or an MDB2 error on failure. + * Some DBMS may not return any columns when the result set + * does not contain any rows. + * @access private + */ + function _getColumnNames() + { + $columns = array(); + $numcols = $this->numCols(); + if (PEAR::isError($numcols)) { + return $numcols; + } + for ($column = 0; $column < $numcols; $column++) { + $column_name = @mysql_field_name($this->result, $column); + $columns[$column_name] = $column; + } + if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $columns = array_change_key_case($columns, $this->db->options['field_case']); + } + return $columns; + } + + // }}} + // {{{ numCols() + + /** + * Count the number of columns returned by the DBMS in a query result. + * + * @return mixed integer value with the number of columns, a MDB2 error + * on failure + * @access public + */ + function numCols() + { + $cols = @mysql_num_fields($this->result); + if (is_null($cols)) { + if ($this->result === false) { + return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'resultset has already been freed', __FUNCTION__); + } elseif (is_null($this->result)) { + return count($this->types); + } + return $this->db->raiseError(null, null, null, + 'Could not get column count', __FUNCTION__); + } + return $cols; + } + + // }}} + // {{{ free() + + /** + * Free the internal resources associated with result. + * + * @return boolean true on success, false if result is invalid + * @access public + */ + function free() + { + if (is_resource($this->result) && $this->db->connection) { + $free = @mysql_free_result($this->result); + if ($free === false) { + return $this->db->raiseError(null, null, null, + 'Could not free result', __FUNCTION__); + } + } + $this->result = false; + return MDB2_OK; + } +} + +/** + * MDB2 MySQL buffered result driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_BufferedResult_mysql extends MDB2_Result_mysql +{ + // }}} + // {{{ seek() + + /** + * Seek to a specific row in a result set + * + * @param int $rownum number of the row where the data can be found + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function seek($rownum = 0) + { + if ($this->rownum != ($rownum - 1) && !@mysql_data_seek($this->result, $rownum)) { + if ($this->result === false) { + return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'resultset has already been freed', __FUNCTION__); + } elseif (is_null($this->result)) { + return MDB2_OK; + } + return $this->db->raiseError(MDB2_ERROR_INVALID, null, null, + 'tried to seek to an invalid row number ('.$rownum.')', __FUNCTION__); + } + $this->rownum = $rownum - 1; + return MDB2_OK; + } + + // }}} + // {{{ valid() + + /** + * Check if the end of the result set has been reached + * + * @return mixed true or false on sucess, a MDB2 error on failure + * @access public + */ + function valid() + { + $numrows = $this->numRows(); + if (PEAR::isError($numrows)) { + return $numrows; + } + return $this->rownum < ($numrows - 1); + } + + // }}} + // {{{ numRows() + + /** + * Returns the number of rows in a result object + * + * @return mixed MDB2 Error Object or the number of rows + * @access public + */ + function numRows() + { + $rows = @mysql_num_rows($this->result); + if (is_null($rows)) { + if ($this->result === false) { + return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'resultset has already been freed', __FUNCTION__); + } elseif (is_null($this->result)) { + return 0; + } + return $this->db->raiseError(null, null, null, + 'Could not get row count', __FUNCTION__); + } + return $rows; + } +} + +/** + * MDB2 MySQL statement driver + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Statement_mysql extends MDB2_Statement_Common +{ + // {{{ _execute() + + /** + * Execute a prepared query statement helper method. + * + * @param mixed $result_class string which specifies which result class to use + * @param mixed $result_wrap_class string which specifies which class to wrap results in + * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure + * @access private + */ + function &_execute($result_class = true, $result_wrap_class = false) + { + if (is_null($this->statement)) { + $result =& parent::_execute($result_class, $result_wrap_class); + return $result; + } + $this->db->last_query = $this->query; + $this->db->debug($this->query, 'execute', array('is_manip' => $this->is_manip, 'when' => 'pre', 'parameters' => $this->values)); + if ($this->db->getOption('disable_query')) { + $result = $this->is_manip ? 0 : null; + return $result; + } + + $connection = $this->db->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + + $query = 'EXECUTE '.$this->statement; + if (!empty($this->positions)) { + $parameters = array(); + foreach ($this->positions as $parameter) { + if (!array_key_exists($parameter, $this->values)) { + return $this->db->raiseError(MDB2_ERROR_NOT_FOUND, null, null, + 'Unable to bind to missing placeholder: '.$parameter, __FUNCTION__); + } + $value = $this->values[$parameter]; + $type = array_key_exists($parameter, $this->types) ? $this->types[$parameter] : null; + if (is_resource($value) || $type == 'clob' || $type == 'blob') { + if (!is_resource($value) && preg_match('/^(\w+:\/\/)(.*)$/', $value, $match)) { + if ($match[1] == 'file://') { + $value = $match[2]; + } + $value = @fopen($value, 'r'); + $close = true; + } + if (is_resource($value)) { + $data = ''; + while (!@feof($value)) { + $data.= @fread($value, $this->db->options['lob_buffer_length']); + } + if ($close) { + @fclose($value); + } + $value = $data; + } + } + $quoted = $this->db->quote($value, $type); + if (PEAR::isError($quoted)) { + return $quoted; + } + $param_query = 'SET @'.$parameter.' = '.$quoted; + $result = $this->db->_doQuery($param_query, true, $connection); + if (PEAR::isError($result)) { + return $result; + } + } + $query.= ' USING @'.implode(', @', array_values($this->positions)); + } + + $result = $this->db->_doQuery($query, $this->is_manip, $connection); + if (PEAR::isError($result)) { + return $result; + } + + if ($this->is_manip) { + $affected_rows = $this->db->_affectedRows($connection, $result); + return $affected_rows; + } + + $result =& $this->db->_wrapResult($result, $this->result_types, + $result_class, $result_wrap_class, $this->limit, $this->offset); + $this->db->debug($this->query, 'execute', array('is_manip' => $this->is_manip, 'when' => 'post', 'result' => $result)); + return $result; + } + + // }}} + // {{{ free() + + /** + * Release resources allocated for the specified prepared query. + * + * @return mixed MDB2_OK on success, a MDB2 error on failure + * @access public + */ + function free() + { + if (is_null($this->positions)) { + return $this->db->raiseError(MDB2_ERROR, null, null, + 'Prepared statement has already been freed', __FUNCTION__); + } + $result = MDB2_OK; + + if (!is_null($this->statement)) { + $connection = $this->db->getConnection(); + if (PEAR::isError($connection)) { + return $connection; + } + $query = 'DEALLOCATE PREPARE '.$this->statement; + $result = $this->db->_doQuery($query, true, $connection); + } + + parent::free(); + return $result; + } +} +?> \ No newline at end of file diff --git a/MDB2/Extended.php b/MDB2/Extended.php new file mode 100644 index 0000000..d52a141 --- /dev/null +++ b/MDB2/Extended.php @@ -0,0 +1,714 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Extended.php,v 1.58 2007/01/06 21:40:52 quipo Exp $ + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +/** + * Used by autoPrepare() + */ +define('MDB2_AUTOQUERY_INSERT', 1); +define('MDB2_AUTOQUERY_UPDATE', 2); +define('MDB2_AUTOQUERY_DELETE', 3); +define('MDB2_AUTOQUERY_SELECT', 4); + +/** + * MDB2_Extended: class which adds several high level methods to MDB2 + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Extended extends MDB2_Module_Common +{ + // {{{ autoPrepare() + + /** + * Generate an insert, update or delete query and call prepare() on it + * + * @param string table + * @param array the fields names + * @param int type of query to build + * MDB2_AUTOQUERY_INSERT + * MDB2_AUTOQUERY_UPDATE + * MDB2_AUTOQUERY_DELETE + * MDB2_AUTOQUERY_SELECT + * @param string (in case of update and delete queries, this string will be put after the sql WHERE statement) + * @param array that contains the types of the placeholders + * @param mixed array that contains the types of the columns in + * the result set or MDB2_PREPARE_RESULT, if set to + * MDB2_PREPARE_MANIP the query is handled as a manipulation query + * + * @return resource handle for the query + * @see buildManipSQL + * @access public + */ + function autoPrepare($table, $table_fields, $mode = MDB2_AUTOQUERY_INSERT, + $where = false, $types = null, $result_types = MDB2_PREPARE_MANIP) + { + $query = $this->buildManipSQL($table, $table_fields, $mode, $where); + if (PEAR::isError($query)) { + return $query; + } + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + return $db->prepare($query, $types, $result_types); + } + // }}} + + // {{{ autoExecute() + + /** + * Generate an insert, update or delete query and call prepare() and execute() on it + * + * @param string name of the table + * @param array assoc ($key=>$value) where $key is a field name and $value its value + * @param int type of query to build + * MDB2_AUTOQUERY_INSERT + * MDB2_AUTOQUERY_UPDATE + * MDB2_AUTOQUERY_DELETE + * MDB2_AUTOQUERY_SELECT + * @param string (in case of update and delete queries, this string will be put after the sql WHERE statement) + * @param array that contains the types of the placeholders + * @param string which specifies which result class to use + * @param mixed array that contains the types of the columns in + * the result set or MDB2_PREPARE_RESULT, if set to + * MDB2_PREPARE_MANIP the query is handled as a manipulation query + * + * @return bool|MDB2_Error true on success, a MDB2 error on failure + * @see buildManipSQL + * @see autoPrepare + * @access public + */ + function &autoExecute($table, $fields_values, $mode = MDB2_AUTOQUERY_INSERT, + $where = false, $types = null, $result_class = true, $result_types = MDB2_PREPARE_MANIP) + { + $fields_values = (array)$fields_values; + if ($mode == MDB2_AUTOQUERY_SELECT) { + if (is_array($result_types)) { + $keys = array_keys($result_types); + } elseif (!empty($fields_values)) { + $keys = $fields_values; + } else { + $keys = array(); + } + } else { + $keys = array_keys($fields_values); + } + $params = array_values($fields_values); + if (empty($params)) { + $query = $this->buildManipSQL($table, $keys, $mode, $where); + + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + if ($mode == MDB2_AUTOQUERY_SELECT) { + $result =& $db->query($query, $result_types, $result_class); + } else { + $result = $db->exec($query); + } + } else { + $stmt = $this->autoPrepare($table, $keys, $mode, $where, $types, $result_types); + if (PEAR::isError($stmt)) { + return $stmt; + } + $result =& $stmt->execute($params, $result_class); + $stmt->free(); + } + return $result; + } + // }}} + + // {{{ buildManipSQL() + + /** + * Make automaticaly an sql query for prepare() + * + * Example : buildManipSQL('table_sql', array('field1', 'field2', 'field3'), MDB2_AUTOQUERY_INSERT) + * will return the string : INSERT INTO table_sql (field1,field2,field3) VALUES (?,?,?) + * NB : - This belongs more to a SQL Builder class, but this is a simple facility + * - Be carefull ! If you don't give a $where param with an UPDATE/DELETE query, all + * the records of the table will be updated/deleted ! + * + * @param string name of the table + * @param ordered array containing the fields names + * @param int type of query to build + * MDB2_AUTOQUERY_INSERT + * MDB2_AUTOQUERY_UPDATE + * MDB2_AUTOQUERY_DELETE + * MDB2_AUTOQUERY_SELECT + * @param string (in case of update and delete queries, this string will be put after the sql WHERE statement) + * + * @return string sql query for prepare() + * @access public + */ + function buildManipSQL($table, $table_fields, $mode, $where = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if ($db->options['quote_identifier']) { + $table = $db->quoteIdentifier($table); + } + + if (!empty($table_fields) && $db->options['quote_identifier']) { + foreach ($table_fields as $key => $field) { + $table_fields[$key] = $db->quoteIdentifier($field); + } + } + + if ($where !== false && !is_null($where)) { + if (is_array($where)) { + $where = implode(' AND ', $where); + } + $where = ' WHERE '.$where; + } + + switch ($mode) { + case MDB2_AUTOQUERY_INSERT: + if (empty($table_fields)) { + return $db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'Insert requires table fields', __FUNCTION__); + } + $cols = implode(', ', $table_fields); + $values = '?'.str_repeat(', ?', (count($table_fields) - 1)); + return 'INSERT INTO '.$table.' ('.$cols.') VALUES ('.$values.')'; + break; + case MDB2_AUTOQUERY_UPDATE: + if (empty($table_fields)) { + return $db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null, + 'Update requires table fields', __FUNCTION__); + } + $set = implode(' = ?, ', $table_fields).' = ?'; + $sql = 'UPDATE '.$table.' SET '.$set.$where; + return $sql; + break; + case MDB2_AUTOQUERY_DELETE: + $sql = 'DELETE FROM '.$table.$where; + return $sql; + break; + case MDB2_AUTOQUERY_SELECT: + $cols = !empty($table_fields) ? implode(', ', $table_fields) : '*'; + $sql = 'SELECT '.$cols.' FROM '.$table.$where; + return $sql; + break; + } + return $db->raiseError(MDB2_ERROR_SYNTAX, null, null, + 'Non existant mode', __FUNCTION__); + } + // }}} + + // {{{ limitQuery() + + /** + * Generates a limited query + * + * @param string query + * @param array that contains the types of the columns in the result set + * @param integer the numbers of rows to fetch + * @param integer the row to start to fetching + * @param string which specifies which result class to use + * @param mixed string which specifies which class to wrap results in + * + * @return MDB2_Result|MDB2_Error result set on success, a MDB2 error on failure + * @access public + */ + function &limitQuery($query, $types, $limit, $offset = 0, $result_class = true, + $result_wrap_class = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + $result = $db->setLimit($limit, $offset); + if (PEAR::isError($result)) { + return $result; + } + $result =& $db->query($query, $types, $result_class, $result_wrap_class); + return $result; + } + // }}} + + // {{{ execParam() + + /** + * Execute a parameterized DML statement. + * + * @param string the SQL query + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * + * @return int|MDB2_Error affected rows on success, a MDB2 error on failure + * @access public + */ + function execParam($query, $params = array(), $param_types = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + if (empty($params)) { + return $db->exec($query); + } + + $stmt = $db->prepare($query, $param_types, MDB2_PREPARE_MANIP); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (PEAR::isError($result)) { + return $result; + } + + $stmt->free(); + return $result; + } + // }}} + + // {{{ getOne() + + /** + * Fetch the first column of the first row of data returned from a query. + * Takes care of doing the query and freeing the results when finished. + * + * @param string the SQL query + * @param string that contains the type of the column in the result set + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * @param int|string which column to return + * + * @return scalar|MDB2_Error data on success, a MDB2 error on failure + * @access public + */ + function getOne($query, $type = null, $params = array(), + $param_types = null, $colnum = 0) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + settype($type, 'array'); + if (empty($params)) { + return $db->queryOne($query, $type, $colnum); + } + + $stmt = $db->prepare($query, $param_types, $type); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $one = $result->fetchOne($colnum); + $stmt->free(); + $result->free(); + return $one; + } + // }}} + + // {{{ getRow() + + /** + * Fetch the first row of data returned from a query. Takes care + * of doing the query and freeing the results when finished. + * + * @param string the SQL query + * @param array that contains the types of the columns in the result set + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * @param int the fetch mode to use + * + * @return array|MDB2_Error data on success, a MDB2 error on failure + * @access public + */ + function getRow($query, $types = null, $params = array(), + $param_types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + if (empty($params)) { + return $db->queryRow($query, $types, $fetchmode); + } + + $stmt = $db->prepare($query, $param_types, $types); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $row = $result->fetchRow($fetchmode); + $stmt->free(); + $result->free(); + return $row; + } + // }}} + + // {{{ getCol() + + /** + * Fetch a single column from a result set and return it as an + * indexed array. + * + * @param string the SQL query + * @param string that contains the type of the column in the result set + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * @param int|string which column to return + * + * @return array|MDB2_Error data on success, a MDB2 error on failure + * @access public + */ + function getCol($query, $type = null, $params = array(), + $param_types = null, $colnum = 0) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + settype($type, 'array'); + if (empty($params)) { + return $db->queryCol($query, $type, $colnum); + } + + $stmt = $db->prepare($query, $param_types, $type); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $col = $result->fetchCol($colnum); + $stmt->free(); + $result->free(); + return $col; + } + // }}} + + // {{{ getAll() + + /** + * Fetch all the rows returned from a query. + * + * @param string the SQL query + * @param array that contains the types of the columns in the result set + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * @param int the fetch mode to use + * @param bool if set to true, the $all will have the first + * column as its first dimension + * @param bool $force_array used only when the query returns exactly + * two columns. If true, the values of the returned array will be + * one-element arrays instead of scalars. + * @param bool $group if true, the values of the returned array is + * wrapped in another array. If the same key value (in the first + * column) repeats itself, the values will be appended to this array + * instead of overwriting the existing values. + * + * @return array|MDB2_Error data on success, a MDB2 error on failure + * @access public + */ + function getAll($query, $types = null, $params = array(), + $param_types = null, $fetchmode = MDB2_FETCHMODE_DEFAULT, + $rekey = false, $force_array = false, $group = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + if (empty($params)) { + return $db->queryAll($query, $types, $fetchmode, $rekey, $force_array, $group); + } + + $stmt = $db->prepare($query, $param_types, $types); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $all = $result->fetchAll($fetchmode, $rekey, $force_array, $group); + $stmt->free(); + $result->free(); + return $all; + } + // }}} + + // {{{ getAssoc() + + /** + * Fetch the entire result set of a query and return it as an + * associative array using the first column as the key. + * + * If the result set contains more than two columns, the value + * will be an array of the values from column 2-n. If the result + * set contains only two columns, the returned value will be a + * scalar with the value of the second column (unless forced to an + * array with the $force_array parameter). A MDB2 error code is + * returned on errors. If the result set contains fewer than two + * columns, a MDB2_ERROR_TRUNCATED error is returned. + * + * For example, if the table 'mytable' contains: + *
+     *   ID      TEXT       DATE
+     * --------------------------------
+     *   1       'one'      944679408
+     *   2       'two'      944679408
+     *   3       'three'    944679408
+     * 
+ * Then the call getAssoc('SELECT id,text FROM mytable') returns: + *
+     *    array(
+     *      '1' => 'one',
+     *      '2' => 'two',
+     *      '3' => 'three',
+     *    )
+     * 
+ * ...while the call getAssoc('SELECT id,text,date FROM mytable') returns: + *
+     *    array(
+     *      '1' => array('one', '944679408'),
+     *      '2' => array('two', '944679408'),
+     *      '3' => array('three', '944679408')
+     *    )
+     * 
+ * + * If the more than one row occurs with the same value in the + * first column, the last row overwrites all previous ones by + * default. Use the $group parameter if you don't want to + * overwrite like this. Example: + *
+     * getAssoc('SELECT category,id,name FROM mytable', null, null
+     *           MDB2_FETCHMODE_ASSOC, false, true) returns:
+     *    array(
+     *      '1' => array(array('id' => '4', 'name' => 'number four'),
+     *                   array('id' => '6', 'name' => 'number six')
+     *             ),
+     *      '9' => array(array('id' => '4', 'name' => 'number four'),
+     *                   array('id' => '6', 'name' => 'number six')
+     *             )
+     *    )
+     * 
+ * + * Keep in mind that database functions in PHP usually return string + * values for results regardless of the database's internal type. + * + * @param string the SQL query + * @param array that contains the types of the columns in the result set + * @param array if supplied, prepare/execute will be used + * with this array as execute parameters + * @param array that contains the types of the values defined in $params + * @param bool $force_array used only when the query returns + * exactly two columns. If TRUE, the values of the returned array + * will be one-element arrays instead of scalars. + * @param bool $group if TRUE, the values of the returned array + * is wrapped in another array. If the same key value (in the first + * column) repeats itself, the values will be appended to this array + * instead of overwriting the existing values. + * + * @return array|MDB2_Error data on success, a MDB2 error on failure + * @access public + */ + function getAssoc($query, $types = null, $params = array(), $param_types = null, + $fetchmode = MDB2_FETCHMODE_DEFAULT, $force_array = false, $group = false) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + settype($params, 'array'); + if (empty($params)) { + return $db->queryAll($query, $types, $fetchmode, true, $force_array, $group); + } + + $stmt = $db->prepare($query, $param_types, $types); + if (PEAR::isError($stmt)) { + return $stmt; + } + + $result = $stmt->execute($params); + if (!MDB2::isResultCommon($result)) { + return $result; + } + + $all = $result->fetchAll($fetchmode, true, $force_array, $group); + $stmt->free(); + $result->free(); + return $all; + } + // }}} + + // {{{ executeMultiple() + + /** + * This function does several execute() calls on the same statement handle. + * $params must be an array indexed numerically from 0, one execute call is + * done for every 'row' in the array. + * + * If an error occurs during execute(), executeMultiple() does not execute + * the unfinished rows, but rather returns that error. + * + * @param resource query handle from prepare() + * @param array numeric array containing the data to insert into the query + * + * @return bool|MDB2_Error true on success, a MDB2 error on failure + * @access public + * @see prepare(), execute() + */ + function executeMultiple(&$stmt, $params = null) + { + for ($i = 0, $j = count($params); $i < $j; $i++) { + $result = $stmt->execute($params[$i]); + if (PEAR::isError($result)) { + return $result; + } + } + return MDB2_OK; + } + // }}} + + // {{{ getBeforeID() + + /** + * Returns the next free id of a sequence if the RDBMS + * does not support auto increment + * + * @param string name of the table into which a new row was inserted + * @param string name of the field into which a new row was inserted + * @param bool when true the sequence is automatic created, if it not exists + * @param bool if the returned value should be quoted + * + * @return int|MDB2_Error id on success, a MDB2 error on failure + * @access public + */ + function getBeforeID($table, $field = null, $ondemand = true, $quote = true) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if ($db->supports('auto_increment') !== true) { + $seq = $table.(empty($field) ? '' : '_'.$field); + $id = $db->nextID($seq, $ondemand); + if (!$quote || PEAR::isError($id)) { + return $id; + } + return $db->quote($id, 'integer'); + } elseif (!$quote) { + return null; + } + return 'NULL'; + } + // }}} + + // {{{ getAfterID() + + /** + * Returns the autoincrement ID if supported or $id + * + * @param mixed value as returned by getBeforeId() + * @param string name of the table into which a new row was inserted + * @param string name of the field into which a new row was inserted + * + * @return int|MDB2_Error id on success, a MDB2 error on failure + * @access public + */ + function getAfterID($id, $table, $field = null) + { + $db =& $this->getDBInstance(); + if (PEAR::isError($db)) { + return $db; + } + + if ($db->supports('auto_increment') !== true) { + return $id; + } + return $db->lastInsertID($table, $field); + } + // }}} +} +?> \ No newline at end of file diff --git a/MDB2/Iterator.php b/MDB2/Iterator.php new file mode 100644 index 0000000..ca5e7b6 --- /dev/null +++ b/MDB2/Iterator.php @@ -0,0 +1,259 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Iterator.php,v 1.22 2006/05/06 14:03:41 lsmith Exp $ + +/** + * PHP5 Iterator + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_Iterator implements Iterator +{ + protected $fetchmode; + protected $result; + protected $row; + + // {{{ constructor + + /** + * Constructor + */ + public function __construct($result, $fetchmode = MDB2_FETCHMODE_DEFAULT) + { + $this->result = $result; + $this->fetchmode = $fetchmode; + } + // }}} + + // {{{ seek() + + /** + * Seek forward to a specific row in a result set + * + * @param int number of the row where the data can be found + * + * @return void + * @access public + */ + public function seek($rownum) + { + $this->row = null; + if ($this->result) { + $this->result->seek($rownum); + } + } + // }}} + + // {{{ next() + + /** + * Fetch next row of data + * + * @return void + * @access public + */ + public function next() + { + $this->row = null; + } + // }}} + + // {{{ current() + + /** + * return a row of data + * + * @return void + * @access public + */ + public function current() + { + if (is_null($this->row)) { + $row = $this->result->fetchRow($this->fetchmode); + if (PEAR::isError($row)) { + $row = false; + } + $this->row = $row; + } + return $this->row; + } + // }}} + + // {{{ valid() + + /** + * Check if the end of the result set has been reached + * + * @return bool true/false, false is also returned on failure + * @access public + */ + public function valid() + { + return (bool)$this->current(); + } + // }}} + + // {{{ free() + + /** + * Free the internal resources associated with result. + * + * @return bool|MDB2_Error true on success, false|MDB2_Error if result is invalid + * @access public + */ + public function free() + { + if ($this->result) { + return $this->result->free(); + } + $this->result = false; + $this->row = null; + return false; + } + // }}} + + // {{{ key() + + /** + * Returns the row number + * + * @return int|bool|MDB2_Error true on success, false|MDB2_Error if result is invalid + * @access public + */ + public function key() + { + if ($this->result) { + return $this->result->rowCount(); + } + return false; + } + // }}} + + // {{{ rewind() + + /** + * Seek to the first row in a result set + * + * @return void + * @access public + */ + public function rewind() + { + } + // }}} + + // {{{ destructor + + /** + * Destructor + */ + public function __destruct() + { + $this->free(); + } + // }}} +} + +/** + * PHP5 buffered Iterator + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_BufferedIterator extends MDB2_Iterator implements SeekableIterator +{ + // {{{ valid() + + /** + * Check if the end of the result set has been reached + * + * @return bool|MDB2_Error true on success, false|MDB2_Error if result is invalid + * @access public + */ + public function valid() + { + if ($this->result) { + return $this->result->valid(); + } + return false; + } + // }}} + + // {{{count() + + /** + * Returns the number of rows in a result object + * + * @return int|MDB2_Error number of rows, false|MDB2_Error if result is invalid + * @access public + */ + public function count() + { + if ($this->result) { + return $this->result->numRows(); + } + return false; + } + // }}} + + // {{{ rewind() + + /** + * Seek to the first row in a result set + * + * @return void + * @access public + */ + public function rewind() + { + $this->seek(0); + } + // }}} +} + +?> \ No newline at end of file diff --git a/MDB2/LOB.php b/MDB2/LOB.php new file mode 100644 index 0000000..69db267 --- /dev/null +++ b/MDB2/LOB.php @@ -0,0 +1,264 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: LOB.php,v 1.34 2006/10/25 11:52:21 lsmith Exp $ + +/** + * @package MDB2 + * @category Database + * @author Lukas Smith + */ + +require_once 'MDB2.php'; + +/** + * MDB2_LOB: user land stream wrapper implementation for LOB support + * + * @package MDB2 + * @category Database + * @author Lukas Smith + */ +class MDB2_LOB +{ + /** + * contains the key to the global MDB2 instance array of the associated + * MDB2 instance + * + * @var integer + * @access protected + */ + var $db_index; + + /** + * contains the key to the global MDB2_LOB instance array of the associated + * MDB2_LOB instance + * + * @var integer + * @access protected + */ + var $lob_index; + + // {{{ stream_open() + + /** + * open stream + * + * @param string specifies the URL that was passed to fopen() + * @param string the mode used to open the file + * @param int holds additional flags set by the streams API + * @param string not used + * + * @return bool + * @access public + */ + function stream_open($path, $mode, $options, &$opened_path) + { + if (!preg_match('/^rb?\+?$/', $mode)) { + return false; + } + $url = parse_url($path); + if (empty($url['host'])) { + return false; + } + $this->db_index = (int)$url['host']; + if (!isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + return false; + } + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + $this->lob_index = (int)$url['user']; + if (!isset($db->datatype->lobs[$this->lob_index])) { + return false; + } + return true; + } + // }}} + + // {{{ stream_read() + + /** + * read stream + * + * @param int number of bytes to read + * + * @return string + * @access public + */ + function stream_read($count) + { + if (isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + $db->datatype->_retrieveLOB($db->datatype->lobs[$this->lob_index]); + + $data = $db->datatype->_readLOB($db->datatype->lobs[$this->lob_index], $count); + $length = strlen($data); + if ($length == 0) { + $db->datatype->lobs[$this->lob_index]['endOfLOB'] = true; + } + $db->datatype->lobs[$this->lob_index]['position'] += $length; + return $data; + } + } + // }}} + + // {{{ stream_write() + + /** + * write stream, note implemented + * + * @param string data + * + * @return int + * @access public + */ + function stream_write($data) + { + return 0; + } + // }}} + + // {{{ stream_tell() + + /** + * return the current position + * + * @return int current position + * @access public + */ + function stream_tell() + { + if (isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + return $db->datatype->lobs[$this->lob_index]['position']; + } + } + // }}} + + // {{{ stream_eof() + + /** + * Check if stream reaches EOF + * + * @return bool + * @access public + */ + function stream_eof() + { + if (!isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + return true; + } + + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + $result = $db->datatype->_endOfLOB($db->datatype->lobs[$this->lob_index]); + if (version_compare(phpversion(), "5.0", ">=") + && version_compare(phpversion(), "5.1", "<") + ) { + return !$result; + } + return $result; + } + // }}} + + // {{{ stream_seek() + + /** + * Seek stream, not implemented + * + * @param int offset + * @param int whence + * + * @return bool + * @access public + */ + function stream_seek($offset, $whence) + { + return false; + } + // }}} + + // {{{ stream_stat() + + /** + * return information about stream + * + * @access public + */ + function stream_stat() + { + if (isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + return array( + 'db_index' => $this->db_index, + 'lob_index' => $this->lob_index, + ); + } + } + // }}} + + // {{{ stream_close() + + /** + * close stream + * + * @access public + */ + function stream_close() + { + if (isset($GLOBALS['_MDB2_databases'][$this->db_index])) { + $db =& $GLOBALS['_MDB2_databases'][$this->db_index]; + if (isset($db->datatype->lobs[$this->lob_index])) { + $db->datatype->_destroyLOB($db->datatype->lobs[$this->lob_index]); + unset($db->datatype->lobs[$this->lob_index]); + } + } + } + // }}} +} + +// register streams wrapper +if (!stream_wrapper_register("MDB2LOB", "MDB2_LOB")) { + MDB2::raiseError(); + return false; +} + +?> diff --git a/Mail.php b/Mail.php new file mode 100644 index 0000000..3a0c1a9 --- /dev/null +++ b/Mail.php @@ -0,0 +1,238 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Mail.php,v 1.17 2006/09/15 03:41:18 jon Exp $ + +require_once 'PEAR.php'; + +/** + * PEAR's Mail:: interface. Defines the interface for implementing + * mailers under the PEAR hierarchy, and provides supporting functions + * useful in multiple mailer backends. + * + * @access public + * @version $Revision: 1.17 $ + * @package Mail + */ +class Mail +{ + /** + * Line terminator used for separating header lines. + * @var string + */ + var $sep = "\r\n"; + + /** + * Provides an interface for generating Mail:: objects of various + * types + * + * @param string $driver The kind of Mail:: object to instantiate. + * @param array $params The parameters to pass to the Mail:: object. + * @return object Mail a instance of the driver class or if fails a PEAR Error + * @access public + */ + function &factory($driver, $params = array()) + { + $driver = strtolower($driver); + @include_once 'Mail/' . $driver . '.php'; + $class = 'Mail_' . $driver; + if (class_exists($class)) { + $mailer = new $class($params); + return $mailer; + } else { + return PEAR::raiseError('Unable to find class for driver ' . $driver); + } + } + + /** + * Implements Mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + * @deprecated use Mail_mail::send instead + */ + function send($recipients, $headers, $body) + { + $this->_sanitizeHeaders($headers); + + // if we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // flatten the headers out. + list(,$text_headers) = Mail::prepareHeaders($headers); + + return mail($recipients, $subject, $body, $text_headers); + + } + + /** + * Sanitize an array of mail headers by removing any additional header + * strings present in a legitimate header's value. The goal of this + * filter is to prevent mail injection attacks. + * + * @param array $headers The associative array of headers to sanitize. + * + * @access private + */ + function _sanitizeHeaders(&$headers) + { + foreach ($headers as $key => $value) { + $headers[$key] = + preg_replace('=((||0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', + null, $value); + } + } + + /** + * Take an array of mail headers and return a string containing + * text usable in sending a message. + * + * @param array $headers The array of headers to prepare, in an associative + * array, where the array key is the header name (ie, + * 'Subject'), and the array value is the header + * value (ie, 'test'). The header produced from those + * values would be 'Subject: test'. + * + * @return mixed Returns false if it encounters a bad address, + * otherwise returns an array containing two + * elements: Any From: address found in the headers, + * and the plain text version of the headers. + * @access private + */ + function prepareHeaders($headers) + { + $lines = array(); + $from = null; + + foreach ($headers as $key => $value) { + if (strcasecmp($key, 'From') === 0) { + include_once 'Mail/RFC822.php'; + $parser = &new Mail_RFC822(); + $addresses = $parser->parseAddressList($value, 'localhost', false); + if (PEAR::isError($addresses)) { + return $addresses; + } + + $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; + + // Reject envelope From: addresses with spaces. + if (strstr($from, ' ')) { + return false; + } + + $lines[] = $key . ': ' . $value; + } elseif (strcasecmp($key, 'Received') === 0) { + $received = array(); + if (is_array($value)) { + foreach ($value as $line) { + $received[] = $key . ': ' . $line; + } + } + else { + $received[] = $key . ': ' . $value; + } + // Put Received: headers at the top. Spam detectors often + // flag messages with Received: headers after the Subject: + // as spam. + $lines = array_merge($received, $lines); + } else { + // If $value is an array (i.e., a list of addresses), convert + // it to a comma-delimited string of its elements (addresses). + if (is_array($value)) { + $value = implode(', ', $value); + } + $lines[] = $key . ': ' . $value; + } + } + + return array($from, join($this->sep, $lines)); + } + + /** + * Take a set of recipients and parse them, returning an array of + * bare addresses (forward paths) that can be passed to sendmail + * or an smtp server with the rcpt to: command. + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. + * + * @return mixed An array of forward paths (bare addresses) or a PEAR_Error + * object if the address list could not be parsed. + * @access private + */ + function parseRecipients($recipients) + { + include_once 'Mail/RFC822.php'; + + // if we're passed an array, assume addresses are valid and + // implode them before parsing. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Parse recipients, leaving out all personal info. This is + // for smtp recipients, etc. All relevant personal information + // should already be in the headers. + $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false); + + // If parseAddressList() returned a PEAR_Error object, just return it. + if (PEAR::isError($addresses)) { + return $addresses; + } + + $recipients = array(); + if (is_array($addresses)) { + foreach ($addresses as $ob) { + $recipients[] = $ob->mailbox . '@' . $ob->host; + } + } + + return $recipients; + } + +} diff --git a/Mail/RFC822.php b/Mail/RFC822.php new file mode 100644 index 0000000..aca910e --- /dev/null +++ b/Mail/RFC822.php @@ -0,0 +1,923 @@ + | +// | Chuck Hagenbuch | +// +-----------------------------------------------------------------------+ + +/** + * RFC 822 Email address list validation Utility + * + * What is it? + * + * This class will take an address string, and parse it into it's consituent + * parts, be that either addresses, groups, or combinations. Nested groups + * are not supported. The structure it returns is pretty straight forward, + * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use + * print_r() to view the structure. + * + * How do I use it? + * + * $address_string = 'My Group: "Richard" (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes + * @author Chuck Hagenbuch + * @version $Revision: 1.23 $ + * @license BSD + * @package Mail + */ +class Mail_RFC822 { + + /** + * The address being parsed by the RFC822 object. + * @var string $address + */ + var $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @var string $default_domain + */ + var $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @var boolean $nestGroups + */ + var $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @var boolean $validate + */ + var $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @var array $addresses + */ + var $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @var array $structure + */ + var $structure = array(); + + /** + * The current error message, if any. + * @var string $error + */ + var $error = null; + + /** + * An internal counter/pointer. + * @var integer $index + */ + var $index = null; + + /** + * The number of groups that have been found in the address list. + * @var integer $num_groups + * @access public + */ + var $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @var boolean $mailRFC822 + */ + var $mailRFC822 = true; + + /** + * A limit after which processing stops + * @var int $limit + */ + var $limit = null; + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (!isset($this) || !isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + // Unfold any long lines in $this->address. + $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); + $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); + + while ($this->address = $this->_splitAddresses($this->address)); + + if ($this->address === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + // Validate each address individually. If we encounter an invalid + // address, stop iterating and return an error immediately. + foreach ($this->addresses as $address) { + $valid = $this->_validateAddress($address); + + if ($valid === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $valid); + } else { + $this->structure[] = $valid; + } + } + + return $this->structure; + } + + /** + * Splits an address into separate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + if (!empty($this->limit) && count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) { + return false; + } + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has an unclosed quotes or not. + * + * @access private + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, false otherwise. + */ + function _hasUnclosedQuotes($string) + { + $string = explode('"', $string); + $string_cnt = count($string); + + for ($i = 0; $i < (count($string) - 1); $i++) + if (substr($string[$i], -1) == '\\') + $string_cnt--; + + return ($string_cnt % 2 === 0); + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + $addresses = array(); + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!count($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + // Trim the whitespace from all of the address strings. + array_map('trim', $addresses); + + // Validate each mailbox. + // Format could be one of: name + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + for ($i = 0; $i < count($addresses); $i++) { + if (!$this->validateMailbox($addresses[$i])) { + if (empty($this->error)) { + $this->error = 'Validation failed for: ' . $addresses[$i]; + } + return false; + } + } + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // Splits on one or more Tab or space. + $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + + $phrase_parts = array(); + while (count($parts) > 0){ + $phrase_parts[] = $this->_splitCheck($parts, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + foreach ($phrase_parts as $part) { + // If quoted string: + if (substr($part, 0, 1) == '"') { + if (!$this->_validateQuotedString($part)) { + return false; + } + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($part)) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1* + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // Validation has been turned off; assume the atom is okay. + return true; + } + + // Check for any char from ASCII 0 - ASCII 127 + if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { + return false; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check, removing quoted characters first. + return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + $comments = array(); + + // Catch any RFC822 comments and store them separately. + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a (. + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +1 is for the trailing ) + $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + } else { + break; + } + } + + foreach ($comments as $comment) { + $mailbox = str_replace("($comment)", '', $mailbox); + } + + $mailbox = trim($mailbox); + + // Check for name + route-addr + if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { + return false; + } + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { + $addr_spec = substr($mailbox, 1, -1); + } else { + $addr_spec = $mailbox; + } + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + foreach ($domains as $domain) { + $domain = str_replace('@', '', trim($domain)); + if (!$this->_validateDomain($domain)) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + foreach ($sub_domains as $sub_domain) { + if (!$this->_validateSubdomain(trim($sub_domain))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + $words = array(); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + foreach ($words as $word) { + // If this word contains an unquoted space, it is invalid. (6.2.4) + if (strpos($word, ' ') && $word[0] !== '"') + { + return false; + } + + if ($this->_validatePhrase(trim($word)) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are in the + * given string. This is APPROXIMATE as it only splits based on a + * comma which has no preceding backslash. Could be useful as + * large amounts of addresses will end up producing *large* + * structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?@. This can be sufficient for most + * people. Optional stricter mode can be utilised which restricts + * mailbox characters allowed to alphanumeric, full stop, hyphen + * and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return false; + } + } + +} diff --git a/Mail/mail.php b/Mail/mail.php new file mode 100644 index 0000000..2273f93 --- /dev/null +++ b/Mail/mail.php @@ -0,0 +1,142 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: mail.php,v 1.18 2006/09/13 05:32:08 jon Exp $ + +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * @package Mail + * @version $Revision: 1.18 $ + */ +class Mail_mail extends Mail { + + /** + * Any arguments to pass to the mail() function. + * @var string + */ + var $_params = ''; + + /** + * Constructor. + * + * Instantiates a new Mail_mail:: object based on the parameters + * passed in. + * + * @param array $params Extra arguments for the mail() function. + */ + function Mail_mail($params = null) + { + /* The other mail implementations accept parameters as arrays. + * In the interest of being consistent, explode an array into + * a string of parameter arguments. */ + if (is_array($params)) { + $this->_params = join(' ', $params); + } else { + $this->_params = $params; + } + + /* Because the mail() function may pass headers as command + * line arguments, we can't guarantee the use of the standard + * "\r\n" separator. Instead, we use the system's native line + * separator. */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail_mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + */ + function send($recipients, $headers, $body) + { + $this->_sanitizeHeaders($headers); + + // If we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + /* + * Also remove the To: header. The mail() function will add its own + * To: header based on the contents of $recipients. + */ + unset($headers['To']); + + // Flatten the headers out. + $headerElements = $this->prepareHeaders($headers); + if (PEAR::isError($headerElements)) { + return $headerElements; + } + list(, $text_headers) = $headerElements; + + /* + * We only use mail()'s optional fifth parameter if the additional + * parameters have been provided and we're not running in safe mode. + */ + if (empty($this->_params) || ini_get('safe_mode')) { + $result = mail($recipients, $subject, $body, $text_headers); + } else { + $result = mail($recipients, $subject, $body, $text_headers, + $this->_params); + } + + /* + * If the mail() function returned failure, we need to create a + * PEAR_Error object and return it instead of the boolean result. + */ + if ($result === false) { + $result = PEAR::raiseError('mail() returned failure'); + } + + return $result; + } + +} diff --git a/Mail/mime.php b/Mail/mime.php new file mode 100644 index 0000000..2286920 --- /dev/null +++ b/Mail/mime.php @@ -0,0 +1,1095 @@ + + * Copyright (c) 2003-2006, PEAR + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Tomas V.V. Cox + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mime.php,v 1.81 2007/06/21 19:08:28 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + * + * This class is based on HTML Mime Mail class from + * Richard Heyes which was based also + * in the mime_mail.class by Tobias Ratschiller + * and Sascha Schumann + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; + +/** + * require Mail_mimePart + * + * Mail_mimePart contains the code required to + * create all the different parts a mail can + * consist of. + */ +require_once 'Mail/mimePart.php'; + + +/** + * The Mail_Mime class provides an OO interface to create MIME + * enabled email messages. This way you can create emails that + * contain plain-text bodies, HTML bodies, attachments, inline + * images and specific headers. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Tomas V.V. Cox + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mime +{ + /** + * Contains the plain text part of the email + * + * @var string + * @access private + */ + var $_txtbody; + + /** + * Contains the html part of the email + * + * @var string + * @access private + */ + var $_htmlbody; + + /** + * contains the mime encoded text + * + * @var string + * @access private + */ + var $_mime; + + /** + * contains the multipart content + * + * @var string + * @access private + */ + var $_multipart; + + /** + * list of the attached images + * + * @var array + * @access private + */ + var $_html_images = array(); + + /** + * list of the attachements + * + * @var array + * @access private + */ + var $_parts = array(); + + /** + * Build parameters + * + * @var array + * @access private + */ + var $_build_params = array(); + + /** + * Headers for the mail + * + * @var array + * @access private + */ + var $_headers = array(); + + /** + * End Of Line sequence (for serialize) + * + * @var string + * @access private + */ + var $_eol; + + + /** + * Constructor function. + * + * @param string $crlf what type of linebreak to use. + * Defaults to "\r\n" + * + * @return void + * + * @access public + */ + function Mail_mime($crlf = "\r\n") + { + $this->_setEOL($crlf); + $this->_build_params = array( + 'head_encoding' => 'quoted-printable', + 'text_encoding' => '7bit', + 'html_encoding' => 'quoted-printable', + '7bit_wrap' => 998, + 'html_charset' => 'ISO-8859-1', + 'text_charset' => 'ISO-8859-1', + 'head_charset' => 'ISO-8859-1' + ); + } + + /** + * wakeup function called by unserialize. It re-sets the EOL constant + * + * @access private + * @return void + */ + function __wakeup() + { + $this->_setEOL($this->_eol); + } + + + /** + * Accessor function to set the body text. Body text is used if + * it's not an html mail being sent or else is used to fill the + * text/plain part that emails clients who don't support + * html should show. + * + * @param string $data Either a string or + * the file name with the contents + * @param bool $isfile If true the first param should be treated + * as a file name, else as a string (default) + * @param bool $append If true the text or file is appended to + * the existing body, else the old body is + * overwritten + * + * @return mixed true on success or PEAR_Error object + * @access public + */ + function setTXTBody($data, $isfile = false, $append = false) + { + if (!$isfile) { + if (!$append) { + $this->_txtbody = $data; + } else { + $this->_txtbody .= $data; + } + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + if (!$append) { + $this->_txtbody = $cont; + } else { + $this->_txtbody .= $cont; + } + } + return true; + } + + /** + * Adds a html part to the mail. + * + * @param string $data either a string or the file name with the + * contents + * @param bool $isfile a flag that determines whether $data is a + * filename, or a string(false, default) + * + * @return bool true on success + * @access public + */ + function setHTMLBody($data, $isfile = false) + { + if (!$isfile) { + $this->_htmlbody = $data; + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + $this->_htmlbody = $cont; + } + + return true; + } + + /** + * Adds an image to the list of embedded images. + * + * @param string $file the image file name OR image data itself + * @param string $c_type the content type + * @param string $name the filename of the image. + * Only used if $file is the image data. + * @param bool $isfile whether $file is a filename or not. + * Defaults to true + * + * @return bool true on success + * @access public + */ + function addHTMLImage($file, $c_type='application/octet-stream', + $name = '', $isfile = true) + { + $filedata = ($isfile === true) ? $this->_file2str($file) + : $file; + if ($isfile === true) { + $filename = ($name == '' ? $file : $name); + } else { + $filename = $name; + } + if (PEAR::isError($filedata)) { + return $filedata; + } + $this->_html_images[] = array( + 'body' => $filedata, + 'name' => $filename, + 'c_type' => $c_type, + 'cid' => md5(uniqid(time())) + ); + return true; + } + + /** + * Adds a file to the list of attachments. + * + * @param string $file The file name of the file to attach + * OR the file contents itself + * @param string $c_type The content type + * @param string $name The filename of the attachment + * Only use if $file is the contents + * @param bool $isfile Whether $file is a filename or not + * Defaults to true + * @param string $encoding The type of encoding to use. + * Defaults to base64. + * Possible values: 7bit, 8bit, base64, + * or quoted-printable. + * @param string $disposition The content-disposition of this file + * Defaults to attachment. + * Possible values: attachment, inline. + * @param string $charset The character set used in the filename + * of this attachment. + * @param string $language The language of the attachment + * @param string $location The RFC 2557.4 location of the attachment + * + * @return mixed true on success or PEAR_Error object + * @access public + */ + function addAttachment($file, + $c_type = 'application/octet-stream', + $name = '', + $isfile = true, + $encoding = 'base64', + $disposition = 'attachment', + $charset = '', + $language = '', + $location = '') + { + $filedata = ($isfile === true) ? $this->_file2str($file) + : $file; + if ($isfile === true) { + // Force the name the user supplied, otherwise use $file + $filename = (strlen($name)) ? $name : $file; + } else { + $filename = $name; + } + if (!strlen($filename)) { + $msg = "The supplied filename for the attachment can't be empty"; + $err = PEAR::raiseError($msg); + return $err; + } + $filename = basename($filename); + if (PEAR::isError($filedata)) { + return $filedata; + } + + $this->_parts[] = array( + 'body' => $filedata, + 'name' => $filename, + 'c_type' => $c_type, + 'encoding' => $encoding, + 'charset' => $charset, + 'language' => $language, + 'location' => $location, + 'disposition' => $disposition + ); + return true; + } + + /** + * Get the contents of the given file name as string + * + * @param string $file_name path of file to process + * + * @return string contents of $file_name + * @access private + */ + function &_file2str($file_name) + { + if (!is_readable($file_name)) { + $err = PEAR::raiseError('File is not readable ' . $file_name); + return $err; + } + if (!$fd = fopen($file_name, 'rb')) { + $err = PEAR::raiseError('Could not open ' . $file_name); + return $err; + } + $filesize = filesize($file_name); + if ($filesize == 0) { + $cont = ""; + } else { + if ($magic_quote_setting = get_magic_quotes_runtime()) { + set_magic_quotes_runtime(0); + } + $cont = fread($fd, $filesize); + if ($magic_quote_setting) { + set_magic_quotes_runtime($magic_quote_setting); + } + } + fclose($fd); + return $cont; + } + + /** + * Adds a text subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * @param string $text The text to add. + * + * @return object The text mimePart object + * @access private + */ + function &_addTextPart(&$obj, $text) + { + $params['content_type'] = 'text/plain'; + $params['encoding'] = $this->_build_params['text_encoding']; + $params['charset'] = $this->_build_params['text_charset']; + if (is_object($obj)) { + $ret = $obj->addSubpart($text, $params); + return $ret; + } else { + $ret = new Mail_mimePart($text, $params); + return $ret; + } + } + + /** + * Adds a html subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The html mimePart object + * @access private + */ + function &_addHtmlPart(&$obj) + { + $params['content_type'] = 'text/html'; + $params['encoding'] = $this->_build_params['html_encoding']; + $params['charset'] = $this->_build_params['html_charset']; + if (is_object($obj)) { + $ret = $obj->addSubpart($this->_htmlbody, $params); + return $ret; + } else { + $ret = new Mail_mimePart($this->_htmlbody, $params); + return $ret; + } + } + + /** + * Creates a new mimePart object, using multipart/mixed as + * the initial content-type and returns it during the + * build process. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addMixedPart() + { + $params = array(); + $params['content_type'] = 'multipart/mixed'; + + //Create empty multipart/mixed Mail_mimePart object to return + $ret = new Mail_mimePart('', $params); + return $ret; + } + + /** + * Adds a multipart/alternative part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addAlternativePart(&$obj) + { + $params['content_type'] = 'multipart/alternative'; + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds a multipart/related part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addRelatedPart(&$obj) + { + $params['content_type'] = 'multipart/related'; + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds an html image subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The image information + * + * @return object The image mimePart object + * @access private + */ + function &_addHtmlImagePart(&$obj, $value) + { + $params['content_type'] = $value['c_type']; + $params['encoding'] = 'base64'; + $params['disposition'] = 'inline'; + $params['dfilename'] = $value['name']; + $params['cid'] = $value['cid']; + + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + + } + + /** + * Adds an attachment subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The attachment information + * + * @return object The image mimePart object + * @access private + */ + function &_addAttachmentPart(&$obj, $value) + { + $params['dfilename'] = $value['name']; + $params['encoding'] = $value['encoding']; + if ($value['charset']) { + $params['charset'] = $value['charset']; + } + if ($value['language']) { + $params['language'] = $value['language']; + } + if ($value['location']) { + $params['location'] = $value['location']; + } + $params['content_type'] = $value['c_type']; + $params['disposition'] = isset($value['disposition']) ? + $value['disposition'] : 'attachment'; + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + } + + /** + * Returns the complete e-mail, ready to send using an alternative + * mail delivery method. Note that only the mailpart that is made + * with Mail_Mime is created. This means that, + * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF + * using the $xtra_headers parameter! + * + * @param string $separation The separation etween these two parts. + * @param array $build_params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $xtra_headers The extra headers that should be passed + * to the &headers() function. + * See that function for more info. + * @param bool $overwrite Overwrite the existing headers with new. + * + * @return string The complete e-mail. + * @access public + */ + function getMessage( + $separation = null, + $build_params = null, + $xtra_headers = null, + $overwrite = false + ) + { + if ($separation === null) { + $separation = MAIL_MIME_CRLF; + } + $body = $this->get($build_params); + $head = $this->txtHeaders($xtra_headers, $overwrite); + $mail = $head . $separation . $body; + return $mail; + } + + + /** + * Builds the multipart message from the list ($this->_parts) and + * returns the mime content. + * + * @param array $build_params Build parameters that change the way the email + * is built. Should be associative. Can contain: + * head_encoding - What encoding to use for the headers. + * Options: quoted-printable or base64 + * Default is quoted-printable + * text_encoding - What encoding to use for plain text + * Options: 7bit, 8bit, + * base64, or quoted-printable + * Default is 7bit + * html_encoding - What encoding to use for html + * Options: 7bit, 8bit, + * base64, or quoted-printable + * Default is quoted-printable + * 7bit_wrap - Number of characters before text is + * wrapped in 7bit encoding + * Default is 998 + * html_charset - The character set to use for html. + * Default is iso-8859-1 + * text_charset - The character set to use for text. + * Default is iso-8859-1 + * head_charset - The character set to use for headers. + * Default is iso-8859-1 + * + * @return string The mime content + * @access public + */ + function &get($build_params = null) + { + if (isset($build_params)) { + while (list($key, $value) = each($build_params)) { + $this->_build_params[$key] = $value; + } + } + + if (isset($this->_headers['From'])){ + $domain = @strstr($this->_headers['From'],'@'); + //Bug #11381: Illegal characters in domain ID + $domain = str_replace(array("<", ">", "&", "(", ")", " ", "\"", "'"), "", $domain); + $domain = urlencode($domain); + foreach($this->_html_images as $i => $img){ + $this->_html_images[$i]['cid'] = $this->_html_images[$i]['cid'] . $domain; + } + } + + if (count($this->_html_images) AND isset($this->_htmlbody)) { + foreach ($this->_html_images as $key => $value) { + $regex = array(); + $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . + preg_quote($value['name'], '#') . '\3#'; + $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' . + preg_quote($value['name'], '#') . '\1\s*\)#'; + + $rep = array(); + $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3'; + $rep[] = 'url(\1cid:' . $value['cid'] . '\2)'; + + $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody); + $this->_html_images[$key]['name'] = + basename($this->_html_images[$key]['name']); + } + } + + $null = null; + $attachments = count($this->_parts) ? true : false; + $html_images = count($this->_html_images) ? true : false; + $html = strlen($this->_htmlbody) ? true : false; + $text = (!$html AND strlen($this->_txtbody)) ? true : false; + + switch (true) { + case $text AND !$attachments: + $message =& $this->_addTextPart($null, $this->_txtbody); + break; + + case !$text AND !$html AND $attachments: + $message =& $this->_addMixedPart(); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $text AND $attachments: + $message =& $this->_addMixedPart(); + $this->_addTextPart($message, $this->_txtbody); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html AND !$attachments AND !$html_images: + if (isset($this->_txtbody)) { + $message =& $this->_addAlternativePart($null); + $this->_addTextPart($message, $this->_txtbody); + $this->_addHtmlPart($message); + } else { + $message =& $this->_addHtmlPart($null); + } + break; + + case $html AND !$attachments AND $html_images: + $message =& $this->_addRelatedPart($null); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($message, $this->_html_images[$i]); + } + break; + + case $html AND $attachments AND !$html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html AND $attachments AND $html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $rel =& $this->_addRelatedPart($alt); + } else { + $rel =& $this->_addRelatedPart($message); + } + $this->_addHtmlPart($rel); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($rel, $this->_html_images[$i]); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + } + + if (isset($message)) { + $output = $message->encode(); + + $this->_headers = array_merge($this->_headers, + $output['headers']); + $body = $output['body']; + return $body; + + } else { + $ret = false; + return $ret; + } + } + + /** + * Returns an array with the headers needed to prepend to the email + * (MIME-Version and Content-Type). Format of argument is: + * $array['header-name'] = 'header-value'; + * + * @param array $xtra_headers Assoc array with any extra headers. + * Optional. + * @param bool $overwrite Overwrite already existing headers. + * + * @return array Assoc array with the mime headers + * @access public + */ + function &headers($xtra_headers = null, $overwrite = false) + { + // Content-Type header should already be present, + // So just add mime version header + $headers['MIME-Version'] = '1.0'; + if (isset($xtra_headers)) { + $headers = array_merge($headers, $xtra_headers); + } + if ($overwrite) { + $this->_headers = array_merge($this->_headers, $headers); + } else { + $this->_headers = array_merge($headers, $this->_headers); + } + + $encodedHeaders = $this->_encodeHeaders($this->_headers); + return $encodedHeaders; + } + + /** + * Get the text version of the headers + * (usefull if you want to use the PHP mail() function) + * + * @param array $xtra_headers Assoc array with any extra headers. + * Optional. + * @param bool $overwrite Overwrite the existing heaers with new. + * + * @return string Plain text headers + * @access public + */ + function txtHeaders($xtra_headers = null, $overwrite = false) + { + $headers = $this->headers($xtra_headers, $overwrite); + + $ret = ''; + foreach ($headers as $key => $val) { + $ret .= "$key: $val" . MAIL_MIME_CRLF; + } + return $ret; + } + + /** + * Sets the Subject header + * + * @param string $subject String to set the subject to. + * + * @return void + * @access public + */ + function setSubject($subject) + { + $this->_headers['Subject'] = $subject; + } + + /** + * Set an email to the From (the sender) header + * + * @param string $email The email address to use + * + * @return void + * @access public + */ + function setFrom($email) + { + $this->_headers['From'] = $email; + } + + /** + * Add an email to the Cc (carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addCc($email) + { + if (isset($this->_headers['Cc'])) { + $this->_headers['Cc'] .= ", $email"; + } else { + $this->_headers['Cc'] = $email; + } + } + + /** + * Add an email to the Bcc (blank carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addBcc($email) + { + if (isset($this->_headers['Bcc'])) { + $this->_headers['Bcc'] .= ", $email"; + } else { + $this->_headers['Bcc'] = $email; + } + } + + /** + * Since the PHP send function requires you to specifiy + * recipients (To: header) separately from the other + * headers, the To: header is not properly encoded. + * To fix this, you can use this public method to + * encode your recipients before sending to the send + * function + * + * @param string $recipients A comma-delimited list of recipients + * + * @return string Encoded data + * @access public + */ + function encodeRecipients($recipients) + { + $input = array("To" => $recipients); + $retval = $this->_encodeHeaders($input); + return $retval["To"] ; + } + + /** + * Encodes a header as per RFC2047 + * + * @param array $input The header data to encode + * @param array $params Extra build parameters + * + * @return array Encoded data + * @access private + */ + function _encodeHeaders($input, $params = array()) + { + + $build_params = $this->_build_params; + while (list($key, $value) = each($params)) { + $build_params[$key] = $value; + } + //$hdr_name: Name of the heaer + //$hdr_value: Full line of header value. + //$hdr_value_out: The recombined $hdr_val-atoms, or the encoded string. + + $useIconv = true; + if (isset($build_params['ignore-iconv'])) { + $useIconv = !$build_params['ignore-iconv']; + } + foreach ($input as $hdr_name => $hdr_value) { + if (preg_match('#([\x80-\xFF]){1}#', $hdr_value)) { + if (function_exists('iconv_mime_encode') && $useIconv) { + $imePrefs = array(); + if ($build_params['head_encoding'] == 'base64') { + $imePrefs['scheme'] = 'B'; + } else { + $imePrefs['scheme'] = 'Q'; + } + $imePrefs['input-charset'] = $build_params['head_charset']; + $imePrefs['output-charset'] = $build_params['head_charset']; + $imePrefs['line-length'] = 74; + $imePrefs['line-break-chars'] = "\r\n"; //Specified in RFC2047 + + $hdr_value = iconv_mime_encode($hdr_name, $hdr_value, $imePrefs); + $hdr_value = preg_replace("#^{$hdr_name}\:\ #", "", $hdr_value); + } elseif ($build_params['head_encoding'] == 'base64') { + //Base64 encoding has been selected. + //Base64 encode the entire string + $hdr_value = base64_encode($hdr_value); + + //Generate the header using the specified params and dynamicly + //determine the maximum length of such strings. + //75 is the value specified in the RFC. The first -2 is there so + //the later regexp doesn't break any of the translated chars. + //The -2 on the first line-regexp is to compensate for the ": " + //between the header-name and the header value + $prefix = '=?' . $build_params['head_charset'] . '?B?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix) - 2; + $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; + + //We can cut base4 every 4 characters, so the real max + //we can get must be rounded down. + $maxLength = $maxLength - ($maxLength % 4); + $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); + + $cutpoint = $maxLength1stLine; + $hdr_value_out = $hdr_value; + $output = ""; + while ($hdr_value_out) { + //Split translated string at every $maxLength + $part = substr($hdr_value_out, 0, $cutpoint); + $hdr_value_out = substr($hdr_value_out, $cutpoint); + $cutpoint = $maxLength; + //RFC 2047 specifies that any split header should + //be seperated by a CRLF SPACE. + if ($output) { + $output .= "\r\n "; + } + $output .= $prefix . $part . $suffix; + } + $hdr_value = $output; + } else { + //quoted-printable encoding has been selected + + //Fix for Bug #10298, Ota Mares + //Check if there is a double quote at beginning or end of + //the string to prevent that an open or closing quote gets + //ignored because it is encapsuled by an encoding pre/suffix. + //Remove the double quote and set the specific prefix or + //suffix variable so that we can concat the encoded string and + //the double quotes back together to get the intended string. + $quotePrefix = $quoteSuffix = ''; + if ($hdr_value{0} == '"') { + $hdr_value = substr($hdr_value, 1); + $quotePrefix = '"'; + } + if ($hdr_value{strlen($hdr_value)-1} == '"') { + $hdr_value = substr($hdr_value, 0, -1); + $quoteSuffix = '"'; + } + + //Generate the header using the specified params and dynamicly + //determine the maximum length of such strings. + //75 is the value specified in the RFC. The -2 is there so + //the later regexp doesn't break any of the translated chars. + //The -2 on the first line-regexp is to compensate for the ": " + //between the header-name and the header value + $prefix = '=?' . $build_params['head_charset'] . '?Q?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix) - 2 - 1; + $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; + $maxLength = $maxLength - 1; + + //Replace all special characters used by the encoder. + $search = array('=', '_', '?', ' '); + $replace = array('=3D', '=5F', '=3F', '_'); + $hdr_value = str_replace($search, $replace, $hdr_value); + + //Replace all extended characters (\x80-xFF) with their + //ASCII values. + $hdr_value = preg_replace('#([\x80-\xFF])#e', + '"=" . strtoupper(dechex(ord("\1")))', + $hdr_value); + + //This regexp will break QP-encoded text at every $maxLength + //but will not break any encoded letters. + $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; + $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; + //Fix for Bug #10298, Ota Mares + //Concat the double quotes and encoded string together + $hdr_value = $quotePrefix . $hdr_value . $quoteSuffix; + + + $hdr_value_out = $hdr_value; + $realMax = $maxLength1stLine + strlen($prefix . $suffix); + if (strlen($hdr_value_out) >= $realMax) { + //Begin with the regexp for the first line. + $reg = $reg1st; + $output = ""; + while ($hdr_value_out) { + //Split translated string at every $maxLength + //But make sure not to break any translated chars. + $found = preg_match($reg, $hdr_value_out, $matches); + + //After this first line, we need to use a different + //regexp for the first line. + $reg = $reg2nd; + + //Save the found part and encapsulate it in the + //prefix & suffix. Then remove the part from the + //$hdr_value_out variable. + if ($found) { + $part = $matches[0]; + $len = strlen($matches[0]); + $hdr_value_out = substr($hdr_value_out, $len); + } else { + $part = $hdr_value_out; + $hdr_value_out = ""; + } + + //RFC 2047 specifies that any split header should + //be seperated by a CRLF SPACE + if ($output) { + $output .= "\r\n "; + } + $output .= $prefix . $part . $suffix; + } + $hdr_value_out = $output; + } else { + $hdr_value_out = $prefix . $hdr_value_out . $suffix; + } + $hdr_value = $hdr_value_out; + } + } + $input[$hdr_name] = $hdr_value; + } + return $input; + } + + /** + * Set the object's end-of-line and define the constant if applicable. + * + * @param string $eol End Of Line sequence + * + * @return void + * @access private + */ + function _setEOL($eol) + { + $this->_eol = $eol; + if (!defined('MAIL_MIME_CRLF')) { + define('MAIL_MIME_CRLF', $this->_eol, true); + } + } + + + +} // End of class diff --git a/Mail/mimeDecode.php b/Mail/mimeDecode.php new file mode 100644 index 0000000..aaa870c --- /dev/null +++ b/Mail/mimeDecode.php @@ -0,0 +1,849 @@ + + * Copyright (c) 2003-2006, PEAR + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mimeDecode.php,v 1.48 2006/12/03 13:43:33 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; + + +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return the structure. + * Returned structure is similar to that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimeDecode extends PEAR +{ + /** + * The raw email to decode + * + * @var string + * @access private + */ + var $_input; + + /** + * The header part of the input + * + * @var string + * @access private + */ + var $_header; + + /** + * The body part of the input + * + * @var string + * @access private + */ + var $_body; + + /** + * If an error occurs, this is used to store the message + * + * @var string + * @access private + */ + var $_error; + + /** + * Flag to determine whether to include bodies in the + * returned object. + * + * @var boolean + * @access private + */ + var $_include_bodies; + + /** + * Flag to determine whether to decode bodies + * + * @var boolean + * @access private + */ + var $_decode_bodies; + + /** + * Flag to determine whether to decode headers + * + * @var boolean + * @access private + */ + var $_decode_headers; + + /** + * Constructor. + * + * Sets up the object, initialise the variables, and splits and + * stores the header and body of the input. + * + * @param string The input to decode + * @access public + */ + function Mail_mimeDecode($input) + { + list($header, $body) = $this->_splitBodyHeader($input); + + $this->_input = $input; + $this->_header = $header; + $this->_body = $body; + $this->_decode_bodies = false; + $this->_include_bodies = true; + } + + /** + * Begins the decoding process. If called statically + * it will create an object and call the decode() method + * of it. + * + * @param array An array of various parameters that determine + * various things: + * include_bodies - Whether to include the body in the returned + * object. + * decode_bodies - Whether to decode the bodies + * of the parts. (Transfer encoding) + * decode_headers - Whether to decode headers + * input - If called statically, this will be treated + * as the input + * @return object Decoded results + * @access public + */ + function decode($params = null) + { + // determine if this method has been called statically + $isStatic = !(isset($this) && get_class($this) == __CLASS__); + + // Have we been called statically? + // If so, create an object and pass details to that. + if ($isStatic AND isset($params['input'])) { + + $obj = new Mail_mimeDecode($params['input']); + $structure = $obj->decode($params); + + // Called statically but no input + } elseif ($isStatic) { + return PEAR::raiseError('Called statically and no input given'); + + // Called via an object + } else { + $this->_include_bodies = isset($params['include_bodies']) ? + $params['include_bodies'] : false; + $this->_decode_bodies = isset($params['decode_bodies']) ? + $params['decode_bodies'] : false; + $this->_decode_headers = isset($params['decode_headers']) ? + $params['decode_headers'] : false; + + $structure = $this->_decode($this->_header, $this->_body); + if ($structure === false) { + $structure = $this->raiseError($this->_error); + } + } + + return $structure; + } + + /** + * Performs the decoding. Decodes the body string passed to it + * If it finds certain content-types it will call itself in a + * recursive fashion + * + * @param string Header section + * @param string Body section + * @return object Results of decoding process + * @access private + */ + function _decode($headers, $body, $default_ctype = 'text/plain') + { + $return = new stdClass; + $return->headers = array(); + $headers = $this->_parseHeaders($headers); + + foreach ($headers as $value) { + if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); + $return->headers[strtolower($value['name'])][] = $value['value']; + + } elseif (isset($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])][] = $value['value']; + + } else { + $return->headers[strtolower($value['name'])] = $value['value']; + } + } + + reset($headers); + while (list($key, $value) = each($headers)) { + $headers[$key]['name'] = strtolower($headers[$key]['name']); + switch ($headers[$key]['name']) { + + case 'content-type': + $content_type = $this->_parseHeaderValue($headers[$key]['value']); + + if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { + $return->ctype_primary = $regs[1]; + $return->ctype_secondary = $regs[2]; + } + + if (isset($content_type['other'])) { + while (list($p_name, $p_value) = each($content_type['other'])) { + $return->ctype_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-disposition': + $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); + $return->disposition = $content_disposition['value']; + if (isset($content_disposition['other'])) { + while (list($p_name, $p_value) = each($content_disposition['other'])) { + $return->d_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-transfer-encoding': + $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); + break; + } + } + + if (isset($content_type)) { + switch (strtolower($content_type['value'])) { + case 'text/plain': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'text/html': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'multipart/parallel': + case 'multipart/appledouble': // Appledouble mail + case 'multipart/report': // RFC1892 + case 'multipart/signed': // PGP + case 'multipart/digest': + case 'multipart/alternative': + case 'multipart/related': + case 'multipart/mixed': + if(!isset($content_type['other']['boundary'])){ + $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; + return false; + } + + $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; + + $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); + for ($i = 0; $i < count($parts); $i++) { + list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); + $part = $this->_decode($part_header, $part_body, $default_ctype); + if($part === false) + $part = $this->raiseError($this->_error); + $return->parts[] = $part; + } + break; + + case 'message/rfc822': + $obj = &new Mail_mimeDecode($body); + $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, + 'decode_bodies' => $this->_decode_bodies, + 'decode_headers' => $this->_decode_headers)); + unset($obj); + break; + + default: + if(!isset($content_transfer_encoding['value'])) + $content_transfer_encoding['value'] = '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null; + break; + } + + } else { + $ctype = explode('/', $default_ctype); + $return->ctype_primary = $ctype[0]; + $return->ctype_secondary = $ctype[1]; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; + } + + return $return; + } + + /** + * Given the output of the above function, this will return an + * array of references to the parts, indexed by mime number. + * + * @param object $structure The structure to go through + * @param string $mime_number Internal use only. + * @return array Mime numbers + */ + function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') + { + $return = array(); + if (!empty($structure->parts)) { + if ($mime_number != '') { + $structure->mime_id = $prepend . $mime_number; + $return[$prepend . $mime_number] = &$structure; + } + for ($i = 0; $i < count($structure->parts); $i++) { + + + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { + $prepend = $prepend . $mime_number . '.'; + $_mime_number = ''; + } else { + $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); + } + + $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); + foreach ($arr as $key => $val) { + $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; + } + } + } else { + if ($mime_number == '') { + $mime_number = '1'; + } + $structure->mime_id = $prepend . $mime_number; + $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; + } + + return $return; + } + + /** + * Given a string containing a header and body + * section, this function will split them (at the first + * blank line) and return them. + * + * @param string Input to split apart + * @return array Contains header and body section + * @access private + */ + function _splitBodyHeader($input) + { + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { + return array($match[1], $match[2]); + } + $this->_error = 'Could not split header and body'; + return false; + } + + /** + * Parse headers given in $input and return + * as assoc array. + * + * @param string Headers to parse + * @return array Contains parsed headers + * @access private + */ + function _parseHeaders($input) + { + + if ($input !== '') { + // Unfold the input + $input = preg_replace("/\r?\n/", "\r\n", $input); + $input = preg_replace("/\r\n(\t| )+/", ' ', $input); + $headers = explode("\r\n", trim($input)); + + foreach ($headers as $value) { + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); + $hdr_value = substr($value, $pos+1); + if($hdr_value[0] == ' ') + $hdr_value = substr($hdr_value, 1); + + $return[] = array( + 'name' => $hdr_name, + 'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value + ); + } + } else { + $return = array(); + } + + return $return; + } + + /** + * Function to parse a header value, + * extract first part, and any secondary + * parts (after ;) This function is not as + * robust as it could be. Eg. header comments + * in the wrong place will probably break it. + * + * @param string Header value to parse + * @return array Contains parsed result + * @access private + */ + function _parseHeaderValue($input) + { + + if (($pos = strpos($input, ';')) !== false) { + + $return['value'] = trim(substr($input, 0, $pos)); + $input = trim(substr($input, $pos+1)); + + if (strlen($input) > 0) { + + // This splits on a semi-colon, if there's no preceeding backslash + // Now works with quoted values; had to glue the \; breaks in PHP + // the regex is already bordering on incomprehensible + $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/'; + preg_match_all($splitRegex, $input, $matches); + $parameters = array(); + for ($i=0; $i_quotedPrintableDecode($input); + break; + + case 'base64': + return base64_decode($input); + break; + + default: + return $input; + } + } + + /** + * Given a quoted-printable string, this + * function will decode and return it. + * + * @param string Input body to decode + * @return string Decoded body + * @access private + */ + function _quotedPrintableDecode($input) + { + // Remove soft line breaks + $input = preg_replace("/=\r?\n/", '', $input); + + // Replace encoded characters + $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); + + return $input; + } + + /** + * Checks the input for uuencoded files and returns + * an array of them. Can be called statically, eg: + * + * $files =& Mail_mimeDecode::uudecode($some_text); + * + * It will check for the begin 666 ... end syntax + * however and won't just blindly decode whatever you + * pass it. + * + * @param string Input body to look for attahcments in + * @return array Decoded bodies, filenames and permissions + * @access public + * @author Unknown + */ + function &uudecode($input) + { + // Find all uuencoded sections + preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); + + for ($j = 0; $j < count($matches[3]); $j++) { + + $str = $matches[3][$j]; + $filename = $matches[2][$j]; + $fileperm = $matches[1][$j]; + + $file = ''; + $str = preg_split("/\r?\n/", trim($str)); + $strlen = count($str); + + for ($i = 0; $i < $strlen; $i++) { + $pos = 1; + $d = 0; + $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); + + while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); + + $pos += 4; + $d += 3; + } + + if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $pos += 3; + $d += 2; + } + + if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + } + } + $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); + } + + return $files; + } + + /** + * getSendArray() returns the arguments required for Mail::send() + * used to build the arguments for a mail::send() call + * + * Usage: + * $mailtext = Full email (for example generated by a template) + * $decoder = new Mail_mimeDecode($mailtext); + * $parts = $decoder->getSendArray(); + * if (!PEAR::isError($parts) { + * list($recipents,$headers,$body) = $parts; + * $mail = Mail::factory('smtp'); + * $mail->send($recipents,$headers,$body); + * } else { + * echo $parts->message; + * } + * @return mixed array of recipeint, headers,body or Pear_Error + * @access public + * @author Alan Knowles + */ + function getSendArray() + { + // prevent warning if this is not set + $this->_decode_headers = FALSE; + $headerlist =$this->_parseHeaders($this->_header); + $to = ""; + if (!$headerlist) { + return $this->raiseError("Message did not contain headers"); + } + foreach($headerlist as $item) { + $header[$item['name']] = $item['value']; + switch (strtolower($item['name'])) { + case "to": + case "cc": + case "bcc": + $to = ",".$item['value']; + default: + break; + } + } + if ($to == "") { + return $this->raiseError("Message did not contain any recipents"); + } + $to = substr($to,1); + return array($to,$header,$this->_body); + } + + /** + * Returns a xml copy of the output of + * Mail_mimeDecode::decode. Pass the output in as the + * argument. This function can be called statically. Eg: + * + * $output = $obj->decode(); + * $xml = Mail_mimeDecode::getXML($output); + * + * The DTD used for this should have been in the package. Or + * alternatively you can get it from cvs, or here: + * http://www.phpguru.org/xmail/xmail.dtd. + * + * @param object Input to convert to xml. This should be the + * output of the Mail_mimeDecode::decode function + * @return string XML version of input + * @access public + */ + function getXML($input) + { + $crlf = "\r\n"; + $output = '' . $crlf . + '' . $crlf . + '' . $crlf . + Mail_mimeDecode::_getXML($input) . + ''; + + return $output; + } + + /** + * Function that does the actual conversion to xml. Does a single + * mimepart at a time. + * + * @param object Input to convert to xml. This is a mimepart object. + * It may or may not contain subparts. + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML($input, $indent = 1) + { + $htab = "\t"; + $crlf = "\r\n"; + $output = ''; + $headers = @(array)$input->headers; + + foreach ($headers as $hdr_name => $hdr_value) { + + // Multiple headers with this name + if (is_array($headers[$hdr_name])) { + for ($i = 0; $i < count($hdr_value); $i++) { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); + } + + // Only one header of this sort + } else { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); + } + } + + if (!empty($input->parts)) { + for ($i = 0; $i < count($input->parts); $i++) { + $output .= $crlf . str_repeat($htab, $indent) . '' . $crlf . + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . + str_repeat($htab, $indent) . '' . $crlf; + } + } elseif (isset($input->body)) { + $output .= $crlf . str_repeat($htab, $indent) . 'body . ']]>' . $crlf; + } + + return $output; + } + + /** + * Helper function to _getXML(). Returns xml of a header. + * + * @param string Name of header + * @param string Value of header + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML_helper($hdr_name, $hdr_value, $indent) + { + $htab = "\t"; + $crlf = "\r\n"; + $return = ''; + + $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); + $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); + + // Sort out any parameters + if (!empty($new_hdr_value['other'])) { + foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { + $params[] = str_repeat($htab, $indent) . $htab . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramname) . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramvalue) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . $crlf; + } + + $params = implode('', $params); + } else { + $params = ''; + } + + $return = str_repeat($htab, $indent) . '
' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_name) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_value['value']) . '' . $crlf . + $params . + str_repeat($htab, $indent) . '
' . $crlf; + + return $return; + } + +} // End of class diff --git a/Mail/mimePart.php b/Mail/mimePart.php new file mode 100644 index 0000000..7ad54e5 --- /dev/null +++ b/Mail/mimePart.php @@ -0,0 +1,439 @@ + + * Copyright (c) 2003-2006, PEAR + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mimePart.php,v 1.25 2007/05/14 21:43:08 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimePart { + + /** + * The encoding type of this part + * + * @var string + * @access private + */ + var $_encoding; + + /** + * An array of subparts + * + * @var array + * @access private + */ + var $_subparts; + + /** + * The output of this part after being built + * + * @var string + * @access private + */ + var $_encoded; + + /** + * Headers for this part + * + * @var array + * @access private + */ + var $_headers; + + /** + * The body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body; + + /** + * Constructor. + * + * Sets up the object. + * + * @param $body - The body of the mime part if any. + * @param $params - An associative array of parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, base64, or quoted-printable + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * dfilename - Optional filename parameter for content disposition + * description - Content description + * charset - Character set to use + * @access public + */ + function Mail_mimePart($body = '', $params = array()) + { + if (!defined('MAIL_MIMEPART_CRLF')) { + define('MAIL_MIMEPART_CRLF', defined('MAIL_MIME_CRLF') ? MAIL_MIME_CRLF : "\r\n", TRUE); + } + + $contentType = array(); + $contentDisp = array(); + foreach ($params as $key => $value) { + switch ($key) { + case 'content_type': + $contentType['type'] = $value; + //$headers['Content-Type'] = $value . (isset($charset) ? '; charset="' . $charset . '"' : ''); + break; + + case 'encoding': + $this->_encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'disposition': + $contentDisp['disp'] = $value; + break; + + case 'dfilename': + $contentDisp['filename'] = $value; + $contentType['name'] = $value; + break; + + case 'description': + $headers['Content-Description'] = $value; + break; + + case 'charset': + $contentType['charset'] = $value; + $contentDisp['charset'] = $value; + break; + + case 'language': + $contentType['language'] = $value; + $contentDisp['language'] = $value; + break; + + case 'location': + $headers['Content-Location'] = $value; + break; + + } + } + if (isset($contentType['type'])) { + $headers['Content-Type'] = $contentType['type']; + if (isset($contentType['name'])) { + $headers['Content-Type'] .= ';' . MAIL_MIMEPART_CRLF; + $headers['Content-Type'] .= $this->_buildHeaderParam('name', $contentType['name'], + isset($contentType['charset']) ? $contentType['charset'] : 'US-ASCII', + isset($contentType['language']) ? $contentType['language'] : NULL); + } elseif (isset($contentType['charset'])) { + $headers['Content-Type'] .= "; charset=\"{$contentType['charset']}\""; + } + } + + + if (isset($contentDisp['disp'])) { + $headers['Content-Disposition'] = $contentDisp['disp']; + if (isset($contentDisp['filename'])) { + $headers['Content-Disposition'] .= ';' . MAIL_MIMEPART_CRLF; + $headers['Content-Disposition'] .= $this->_buildHeaderParam('filename', $contentDisp['filename'], + isset($contentDisp['charset']) ? $contentDisp['charset'] : 'US-ASCII', + isset($contentDisp['language']) ? $contentDisp['language'] : NULL); + } + } + + + + + // Default content-type + if (!isset($headers['Content-Type'])) { + $headers['Content-Type'] = 'text/plain'; + } + + //Default encoding + if (!isset($this->_encoding)) { + $this->_encoding = '7bit'; + } + + // Assign stuff to member variables + $this->_encoded = array(); + $this->_headers = $headers; + $this->_body = $body; + } + + /** + * encode() + * + * Encodes and returns the email. Also stores + * it in the encoded member variable + * + * @return An associative array containing two elements, + * body and headers. The headers element is itself + * an indexed array. + * @access public + */ + function encode() + { + $encoded =& $this->_encoded; + + if (count($this->_subparts)) { + srand((double)microtime()*1000000); + $boundary = '=_' . md5(rand() . microtime()); + $this->_headers['Content-Type'] .= ';' . MAIL_MIMEPART_CRLF . "\t" . 'boundary="' . $boundary . '"'; + + // Add body parts to $subparts + for ($i = 0; $i < count($this->_subparts); $i++) { + $headers = array(); + $tmp = $this->_subparts[$i]->encode(); + foreach ($tmp['headers'] as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $subparts[] = implode(MAIL_MIMEPART_CRLF, $headers) . MAIL_MIMEPART_CRLF . MAIL_MIMEPART_CRLF . $tmp['body'] . MAIL_MIMEPART_CRLF; + } + + $encoded['body'] = '--' . $boundary . MAIL_MIMEPART_CRLF . + rtrim(implode('--' . $boundary . MAIL_MIMEPART_CRLF , $subparts), MAIL_MIMEPART_CRLF) . MAIL_MIMEPART_CRLF . + '--' . $boundary.'--' . MAIL_MIMEPART_CRLF; + + } else { + $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); + } + + // Add headers to $encoded + $encoded['headers'] =& $this->_headers; + + return $encoded; + } + + /** + * &addSubPart() + * + * Adds a subpart to current mime part and returns + * a reference to it + * + * @param $body The body of the subpart, if any. + * @param $params The parameters for the subpart, same + * as the $params argument for constructor. + * @return A reference to the part you just added. It is + * crucial if using multipart/* in your subparts that + * you use =& in your script when calling this function, + * otherwise you will not be able to add further subparts. + * @access public + */ + function &addSubPart($body, $params) + { + $this->_subparts[] = new Mail_mimePart($body, $params); + return $this->_subparts[count($this->_subparts) - 1]; + } + + /** + * _getEncodedData() + * + * Returns encoded data based upon encoding passed to it + * + * @param $data The data to encode. + * @param $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * @access private + */ + function _getEncodedData($data, $encoding) + { + switch ($encoding) { + case '8bit': + case '7bit': + return $data; + break; + + case 'quoted-printable': + return $this->_quotedPrintableEncode($data); + break; + + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, MAIL_MIMEPART_CRLF)); + break; + + default: + return $data; + } + } + + /** + * quotedPrintableEncode() + * + * Encodes data to quoted-printable standard. + * + * @param $input The data to encode + * @param $line_max Optional max line length. Should + * not be more than 76 chars + * + * @access private + */ + function _quotedPrintableEncode($input , $line_max = 76) + { + $lines = preg_split("/\r?\n/", $input); + $eol = MAIL_MIMEPART_CRLF; + $escape = '='; + $output = ''; + + while (list(, $line) = each($lines)) { + + $line = preg_split('||', $line, -1, PREG_SPLIT_NO_EMPTY); + $linlen = count($line); + $newline = ''; + + for ($i = 0; $i < $linlen; $i++) { + $char = $line[$i]; + $dec = ord($char); + + if (($dec == 32) AND ($i == ($linlen - 1))) { // convert space at eol only + $char = '=20'; + + } elseif (($dec == 9) AND ($i == ($linlen - 1))) { // convert tab at eol only + $char = '=09'; + } elseif ($dec == 9) { + ; // Do nothing if a tab. + } elseif (($dec == 61) OR ($dec < 32 ) OR ($dec > 126)) { + $char = $escape . strtoupper(sprintf('%02s', dechex($dec))); + } elseif (($dec == 46) AND ($newline == '')) { + //Bug #9722: convert full-stop at bol + //Some Windows servers need this, won't break anything (cipri) + $char = '=2E'; + } + + if ((strlen($newline) + strlen($char)) >= $line_max) { // MAIL_MIMEPART_CRLF is not counted + $output .= $newline . $escape . $eol; // soft line break; " =\r\n" is okay + $newline = ''; + } + $newline .= $char; + } // end of for + $output .= $newline . $eol; + } + $output = substr($output, 0, -1 * strlen($eol)); // Don't want last crlf + return $output; + } + + /** + * _buildHeaderParam() + * + * Encodes the paramater of a header. + * + * @param $name The name of the header-parameter + * @param $value The value of the paramter + * @param $charset The characterset of $value + * @param $language The language used in $value + * @param $maxLength The maximum length of a line. Defauls to 75 + * + * @access private + */ + function _buildHeaderParam($name, $value, $charset=NULL, $language=NULL, $maxLength=75) + { + //If we find chars to encode, or if charset or language + //is not any of the defaults, we need to encode the value. + $shouldEncode = 0; + $secondAsterisk = ''; + if (preg_match('#([\x80-\xFF]){1}#', $value)) { + $shouldEncode = 1; + } elseif ($charset && (strtolower($charset) != 'us-ascii')) { + $shouldEncode = 1; + } elseif ($language && ($language != 'en' && $language != 'en-us')) { + $shouldEncode = 1; + } + if ($shouldEncode) { + $search = array('%', ' ', "\t"); + $replace = array('%25', '%20', '%09'); + $encValue = str_replace($search, $replace, $value); + $encValue = preg_replace('#([\x80-\xFF])#e', '"%" . strtoupper(dechex(ord("\1")))', $encValue); + $value = "$charset'$language'$encValue"; + $secondAsterisk = '*'; + } + $header = " {$name}{$secondAsterisk}=\"{$value}\"; "; + if (strlen($header) <= $maxLength) { + return $header; + } + + $preLength = strlen(" {$name}*0{$secondAsterisk}=\""); + $sufLength = strlen("\";"); + $maxLength = MAX(16, $maxLength - $preLength - $sufLength - 2); + $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; + + $headers = array(); + $headCount = 0; + while ($value) { + $matches = array(); + $found = preg_match($maxLengthReg, $value, $matches); + if ($found) { + $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$matches[0]}\""; + $value = substr($value, strlen($matches[0])); + } else { + $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$value}\""; + $value = ""; + } + $headCount++; + } + $headers = implode(MAIL_MIMEPART_CRLF, $headers) . ';'; + return $headers; + } +} // End of class diff --git a/Mail/null.php b/Mail/null.php new file mode 100644 index 0000000..982bfa4 --- /dev/null +++ b/Mail/null.php @@ -0,0 +1,60 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $ +// + +/** + * Null implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.2 $ + */ +class Mail_null extends Mail { + + /** + * Implements Mail_null::send() function. Silently discards all + * mail. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + return true; + } + +} diff --git a/Mail/sendmail.php b/Mail/sendmail.php new file mode 100644 index 0000000..8d6e385 --- /dev/null +++ b/Mail/sendmail.php @@ -0,0 +1,155 @@ + | +// +----------------------------------------------------------------------+ + +/** + * Sendmail implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.17 $ + */ +class Mail_sendmail extends Mail { + + /** + * The location of the sendmail or sendmail wrapper binary on the + * filesystem. + * @var string + */ + var $sendmail_path = '/usr/sbin/sendmail'; + + /** + * Any extra command-line parameters to pass to the sendmail or + * sendmail wrapper binary. + * @var string + */ + var $sendmail_args = '-i'; + + /** + * Constructor. + * + * Instantiates a new Mail_sendmail:: object based on the parameters + * passed in. It looks for the following parameters: + * sendmail_path The location of the sendmail binary on the + * filesystem. Defaults to '/usr/sbin/sendmail'. + * + * sendmail_args Any extra parameters to pass to the sendmail + * or sendmail wrapper binary. + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array $params Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_sendmail($params) + { + if (isset($params['sendmail_path'])) { + $this->sendmail_path = $params['sendmail_path']; + } + if (isset($params['sendmail_args'])) { + $this->sendmail_args = $params['sendmail_args']; + } + + /* + * Because we need to pass message headers to the sendmail program on + * the commandline, we can't guarantee the use of the standard "\r\n" + * separator. Instead, we use the system's native line separator. + */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail::send() function using the sendmail + * command-line binary. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + $recipients = $this->parseRecipients($recipients); + if (PEAR::isError($recipients)) { + return $recipients; + } + $recipients = escapeShellCmd(implode(' ', $recipients)); + + $this->_sanitizeHeaders($headers); + $headerElements = $this->prepareHeaders($headers); + if (PEAR::isError($headerElements)) { + return $headerElements; + } + list($from, $text_headers) = $headerElements; + + if (!isset($from)) { + return PEAR::raiseError('No from address given.'); + } elseif (strpos($from, ' ') !== false || + strpos($from, ';') !== false || + strpos($from, '&') !== false || + strpos($from, '`') !== false) { + return PEAR::raiseError('From address specified with dangerous characters.'); + } + + $from = escapeShellCmd($from); + $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); + if (!$mail) { + return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); + } + + // Write the headers following by two newlines: one to end the headers + // section and a second to separate the headers block from the body. + fputs($mail, $text_headers . $this->sep . $this->sep); + + fputs($mail, $body); + $result = pclose($mail); + if (version_compare(phpversion(), '4.2.3') == -1) { + // With older php versions, we need to shift the pclose + // result to get the exit code. + $result = $result >> 8 & 0xFF; + } + + if ($result != 0) { + return PEAR::raiseError('sendmail returned error code ' . $result, + $result); + } + + return true; + } + +} diff --git a/Mail/smtp.php b/Mail/smtp.php new file mode 100644 index 0000000..14ae3cd --- /dev/null +++ b/Mail/smtp.php @@ -0,0 +1,348 @@ + | +// | Jon Parise | +// +----------------------------------------------------------------------+ + +/** Error: Failed to create a Net_SMTP object */ +define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); + +/** Error: Failed to connect to SMTP server */ +define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); + +/** Error: SMTP authentication failure */ +define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); + +/** Error: No From: address has been provided */ +define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); + +/** Error: Failed to set sender */ +define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); + +/** Error: Failed to add recipient */ +define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); + +/** Error: Failed to send data */ +define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); + +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * @access public + * @package Mail + * @version $Revision: 1.28 $ + */ +class Mail_smtp extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The SMTP host to connect to. + * @var string + */ + var $host = 'localhost'; + + /** + * The port the SMTP server is on. + * @var integer + */ + var $port = 25; + + /** + * Should SMTP authentication be used? + * + * This value may be set to true, false or the name of a specific + * authentication method. + * + * If the value is set to true, the Net_SMTP package will attempt to use + * the best authentication method advertised by the remote SMTP server. + * + * @var mixed + */ + var $auth = false; + + /** + * The username to use if the SMTP server requires authentication. + * @var string + */ + var $username = ''; + + /** + * The password to use if the SMTP server requires authentication. + * @var string + */ + var $password = ''; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + */ + var $localhost = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = null; + + /** + * Whether to use VERP or not. If not a boolean, the string value + * will be used as the VERP separators. + * + * @var mixed boolean or string + */ + var $verp = false; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $debug + */ + var $debug = false; + + /** + * Indicates whether or not the SMTP connection should persist over + * multiple calls to the send() method. + * + * @var boolean + */ + var $persist = false; + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * host The server to connect to. Defaults to localhost. + * port The port to connect to. Defaults to 25. + * auth SMTP authentication. Defaults to none. + * username The username to use for SMTP auth. No default. + * password The password to use for SMTP auth. No default. + * localhost The local hostname / domain. Defaults to localhost. + * timeout The SMTP connection timeout. Defaults to none. + * verp Whether to use VERP or not. Defaults to false. + * debug Activate SMTP debug mode? Defaults to false. + * persist Should the SMTP connection persist? + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_smtp($params) + { + if (isset($params['host'])) $this->host = $params['host']; + if (isset($params['port'])) $this->port = $params['port']; + if (isset($params['auth'])) $this->auth = $params['auth']; + if (isset($params['username'])) $this->username = $params['username']; + if (isset($params['password'])) $this->password = $params['password']; + if (isset($params['localhost'])) $this->localhost = $params['localhost']; + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['verp'])) $this->verp = $params['verp']; + if (isset($params['debug'])) $this->debug = (boolean)$params['debug']; + if (isset($params['persist'])) $this->persist = (boolean)$params['persist']; + + register_shutdown_function(array(&$this, '_Mail_smtp')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function _Mail_smtp() + { + $this->disconnect(); + } + + /** + * Implements Mail::send() function using SMTP. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (e.g., 'Subject'), and the array value + * is the header value (e.g., 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + include_once 'Net/SMTP.php'; + + /* If we don't already have an SMTP object, create one. */ + if (is_object($this->_smtp) === false) { + $this->_smtp =& new Net_SMTP($this->host, $this->port, + $this->localhost); + + /* If we still don't have an SMTP object at this point, fail. */ + if (is_object($this->_smtp) === false) { + return PEAR::raiseError('Failed to create a Net_SMTP object', + PEAR_MAIL_SMTP_ERROR_CREATE); + } + + /* Configure the SMTP connection. */ + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + /* Attempt to connect to the configured SMTP server. */ + if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { + $error = $this->_error('Failed to connect to ' . + $this->host . ':' . $this->port, + $res); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); + } + + /* Attempt to authenticate if authentication has been enabled. */ + if ($this->auth) { + $method = is_string($this->auth) ? $this->auth : ''; + + if (PEAR::isError($res = $this->_smtp->auth($this->username, + $this->password, + $method))) { + $error = $this->_error("$method authentication failure", + $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); + } + } + } + + $this->_sanitizeHeaders($headers); + $headerElements = $this->prepareHeaders($headers); + if (PEAR::isError($headerElements)) { + $this->_smtp->rset(); + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + $this->_smtp->rset(); + return PEAR::raiseError('No From: address has been provided', + PEAR_MAIL_SMTP_ERROR_FROM); + } + + $args['verp'] = $this->verp; + if (PEAR::isError($res = $this->_smtp->mailFrom($from, $args))) { + $error = $this->_error("Failed to set sender: $from", $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); + } + + $recipients = $this->parseRecipients($recipients); + if (PEAR::isError($recipients)) { + $this->_smtp->rset(); + return $recipients; + } + + foreach ($recipients as $recipient) { + if (PEAR::isError($res = $this->_smtp->rcptTo($recipient))) { + $error = $this->_error("Failed to add recipient: $recipient", + $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); + } + } + + /* Send the message's headers and the body as SMTP data. */ + if (PEAR::isError($res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body))) { + $error = $this->_error('Failed to send data', $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); + } + + /* If persistent connections are disabled, destroy our SMTP object. */ + if ($this->persist === false) { + $this->disconnect(); + } + + return true; + } + + /** + * Disconnect and destroy the current SMTP connection. + * + * @return boolean True if the SMTP connection no longer exists. + * + * @since 1.1.9 + * @access public + */ + function disconnect() + { + /* If we have an SMTP object, disconnect and destroy it. */ + if (is_object($this->_smtp) && $this->_smtp->disconnect()) { + $this->_smtp = null; + } + + /* We are disconnected if we no longer have an SMTP object. */ + return ($this->_smtp === null); + } + + /** + * Build a standardized string describing the current SMTP error. + * + * @param string $text Custom string describing the error context. + * @param object $error Reference to the current PEAR_Error object. + * + * @return string A string describing the current SMTP error. + * + * @since 1.1.7 + * @access private + */ + function _error($text, &$error) + { + /* Split the SMTP response into a code and a response string. */ + list($code, $response) = $this->_smtp->getResponse(); + + /* Build our standardized error string. */ + $msg = $text; + $msg .= ' [SMTP: ' . $error->getMessage(); + $msg .= " (code: $code, response: $response)]"; + + return $msg; + } + +} diff --git a/Net/SMTP.php b/Net/SMTP.php new file mode 100644 index 0000000..69eb155 --- /dev/null +++ b/Net/SMTP.php @@ -0,0 +1,1034 @@ + | +// | Jon Parise | +// | Damian Alejandro Fernandez Sosa | +// +----------------------------------------------------------------------+ +// +// $Id: SMTP.php,v 1.58 2007/03/28 04:53:34 chagenbu Exp $ + +require_once 'PEAR.php'; +require_once 'Net/Socket.php'; + +/** + * Provides an implementation of the SMTP protocol using PEAR's + * Net_Socket:: class. + * + * @package Net_SMTP + * @author Chuck Hagenbuch + * @author Jon Parise + * @author Damian Alejandro Fernandez Sosa + * + * @example basic.php A basic implementation of the Net_SMTP package. + */ +class Net_SMTP +{ + /** + * The server to connect to. + * @var string + * @access public + */ + var $host = 'localhost'; + + /** + * The port to connect to. + * @var int + * @access public + */ + var $port = 25; + + /** + * The value to give when sending EHLO or HELO. + * @var string + * @access public + */ + var $localhost = 'localhost'; + + /** + * List of supported authentication methods, in preferential order. + * @var array + * @access public + */ + var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); + + /** + * Should debugging output be enabled? + * @var boolean + * @access private + */ + var $_debug = false; + + /** + * The socket resource being used to connect to the SMTP server. + * @var resource + * @access private + */ + var $_socket = null; + + /** + * The most recent server response code. + * @var int + * @access private + */ + var $_code = -1; + + /** + * The most recent server response arguments. + * @var array + * @access private + */ + var $_arguments = array(); + + /** + * Stores detected features of the SMTP server. + * @var array + * @access private + */ + var $_esmtp = array(); + + /** + * Instantiates a new Net_SMTP object, overriding any defaults + * with parameters that are passed in. + * + * If you have SSL support in PHP, you can connect to a server + * over SSL using an 'ssl://' prefix: + * + * // 465 is a common smtps port. + * $smtp = new Net_SMTP('ssl://mail.host.com', 465); + * $smtp->connect(); + * + * @param string $host The server to connect to. + * @param integer $port The port to connect to. + * @param string $localhost The value to give when sending EHLO or HELO. + * + * @access public + * @since 1.0 + */ + function Net_SMTP($host = null, $port = null, $localhost = null) + { + if (isset($host)) $this->host = $host; + if (isset($port)) $this->port = $port; + if (isset($localhost)) $this->localhost = $localhost; + + $this->_socket = new Net_Socket(); + + /* + * Include the Auth_SASL package. If the package is not available, + * we disable the authentication methods that depend upon it. + */ + if ((@include_once 'Auth/SASL.php') === false) { + $pos = array_search('DIGEST-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + $pos = array_search('CRAM-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + } + } + + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + * + * @access public + * @since 1.1.0 + */ + function setDebug($debug) + { + $this->_debug = $debug; + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access private + * @since 1.1.0 + */ + function _send($data) + { + if ($this->_debug) { + echo "DEBUG: Send: $data\n"; + } + + if (PEAR::isError($error = $this->_socket->write($data))) { + return PEAR::raiseError('Failed to write to socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Send a command to the server with an optional string of + * arguments. A carriage return / linefeed (CRLF) sequence will + * be appended to each command string before it is sent to the + * SMTP server - an error will be thrown if the command string + * already contains any newline characters. Use _send() for + * commands that must contain newlines. + * + * @param string $command The SMTP command to send to the server. + * @param string $args A string of optional arguments to append + * to the command. + * + * @return mixed The result of the _send() call. + * + * @access private + * @since 1.1.0 + */ + function _put($command, $args = '') + { + if (!empty($args)) { + $command .= ' ' . $args; + } + + if (strcspn($command, "\r\n") !== strlen($command)) { + return PEAR::raiseError('Commands cannot contain newlines'); + } + + return $this->_send($command . "\r\n"); + } + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * + * @return mixed True if the server returned a valid response code or + * a PEAR_Error object is an error condition is reached. + * + * @access private + * @since 1.1.0 + * + * @see getResponse + */ + function _parseResponse($valid) + { + $this->_code = -1; + $this->_arguments = array(); + + while ($line = $this->_socket->readLine()) { + if ($this->_debug) { + echo "DEBUG: Recv: $line\n"; + } + + /* If we receive an empty line, the connection has been closed. */ + if (empty($line)) { + $this->disconnect(); + return PEAR::raiseError('Connection was unexpectedly closed'); + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + + /* Compare the server's response code with the valid code. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } + + /* If we were given an array of valid response codes, check each one. */ + if (is_array($valid)) { + foreach ($valid as $valid_code) { + if ($this->_code === $valid_code) { + return true; + } + } + } + + return PEAR::raiseError('Invalid response code received from server', + $this->_code); + } + + /** + * Return a 2-tuple containing the last response from the SMTP server. + * + * @return array A two-element array: the first element contains the + * response code as an integer and the second element + * contains the response's arguments as a string. + * + * @access public + * @since 1.1.0 + */ + function getResponse() + { + return array($this->_code, join("\n", $this->_arguments)); + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection. + * @param bool $persistent Should a persistent socket connection + * be used? + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function connect($timeout = null, $persistent = false) + { + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout); + if (PEAR::isError($result)) { + return PEAR::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + if (PEAR::isError($error = $this->_parseResponse(220))) { + return $error; + } + if (PEAR::isError($error = $this->_negotiate())) { + return $error; + } + + return true; + } + + /** + * Attempt to disconnect from the SMTP server. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function disconnect() + { + if (PEAR::isError($error = $this->_put('QUIT'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(221))) { + return $error; + } + if (PEAR::isError($error = $this->_socket->disconnect())) { + return PEAR::raiseError('Failed to disconnect socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Attempt to send the EHLO command and obtain a list of ESMTP + * extensions available, and failing that just send HELO. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access private + * @since 1.1.0 + */ + function _negotiate() + { + if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) { + return $error; + } + + if (PEAR::isError($this->_parseResponse(250))) { + /* If we receive a 503 response, we're already authenticated. */ + if ($this->_code === 503) { + return true; + } + + /* If the EHLO failed, try the simpler HELO command. */ + if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) { + return $error; + } + if (PEAR::isError($this->_parseResponse(250))) { + return PEAR::raiseError('HELO was not accepted: ', $this->_code); + } + + return true; + } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + return true; + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @return mixed Returns a string containing the name of the best + * supported authentication method or a PEAR_Error object + * if a failure condition is encountered. + * @access private + * @since 1.1.0 + */ + function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + + foreach ($this->auth_methods as $method) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return PEAR::raiseError('No supported authentication methods'); + } + + /** + * Attempt to do SMTP 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 + * @since 1.0 + */ + function auth($uid, $pwd , $method = '') + { + if (empty($this->_esmtp['AUTH'])) { + if (version_compare(PHP_VERSION, '5.1.0', '>=')) { + if (!isset($this->_esmtp['STARTTLS'])) { + return PEAR::raiseError('SMTP 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 EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + if (empty($this->_esmtp['AUTH'])) { + return PEAR::raiseError('SMTP server does not support authentication'); + } + } else { + return PEAR::raiseError('SMTP server does not support authentication'); + } + } + + /* If no method has been specified, get the name of the best + * supported method advertised by the SMTP server. */ + if (empty($method)) { + if (PEAR::isError($method = $this->_getBestAuthMethod())) { + /* Return the PEAR_Error object from _getBestAuthMethod(). */ + return $method; + } + } else { + $method = strtoupper($method); + if (!in_array($method, $this->auth_methods)) { + return PEAR::raiseError("$method is not a supported authentication 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 = PEAR::raiseError("$method is not a supported authentication method"); + break; + } + + /* If an error was encountered, return the PEAR_Error object. */ + if (PEAR::isError($result)) { + return $result; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authDigest_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $digest = &Auth_SASL::factory('digestmd5'); + $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, + $this->host, "smtp")); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + /* We don't use the protocol's third step because SMTP doesn't + * allow subsequent authentication, so we just silently ignore + * it. */ + if (PEAR::isError($error = $this->_put(' '))) { + return $error; + } + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authCRAM_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $cram = &Auth_SASL::factory('crammd5'); + $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authLogin($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($uid)))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authPlain($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Send the HELO command. + * + * @param string The domain name to say we are. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function helo($domain) + { + if (PEAR::isError($error = $this->_put('HELO', $domain))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Send the MAIL FROM: command. + * + * @param string $sender The sender (reverse path) to set. + * @param string $params String containing additional MAIL parameters, + * such as the NOTIFY flags defined by RFC 1891 + * or the VERP protocol. + * + * If $params is an array, only the 'verp' option + * is supported. If 'verp' is true, the XVERP + * parameter is appended to the MAIL command. If + * the 'verp' value is a string, the full + * XVERP=value parameter is appended. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function mailFrom($sender, $params = null) + { + $args = "FROM:<$sender>"; + + /* Support the deprecated array form of $params. */ + if (is_array($params) && isset($params['verp'])) { + /* XVERP */ + if ($params['verp'] === true) { + $args .= ' XVERP'; + + /* XVERP=something */ + } elseif (trim($params['verp'])) { + $args .= ' XVERP=' . $params['verp']; + } + } elseif (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('MAIL', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Send the RCPT TO: command. + * + * @param string $recipient The recipient (forward path) to add. + * @param string $params String containing additional RCPT parameters, + * such as the NOTIFY flags defined by RFC 1891. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + */ + function rcptTo($recipient, $params = null) + { + $args = "TO:<$recipient>"; + if (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('RCPT', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 251)))) { + return $error; + } + + return true; + } + + /** + * Quote the data so that it meets SMTP standards. + * + * This is provided as a separate public function to facilitate + * easier overloading for the cases where it is desirable to + * customize the quoting behavior. + * + * @param string $data The message text to quote. The string must be passed + * by reference, and the text will be modified in place. + * + * @access public + * @since 1.2 + */ + function quotedata(&$data) + { + /* Change Unix (\n) and Mac (\r) linefeeds into + * Internet-standard CRLF (\r\n) linefeeds. */ + $data = preg_replace(array('/(?_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { + if (strlen($data) >= $this->_esmtp['SIZE']) { + $this->disconnect(); + return PEAR::raiseError('Message size excedes the server limit'); + } + } + + /* Quote the data based on the SMTP standards. */ + $this->quotedata($data); + + if (PEAR::isError($error = $this->_put('DATA'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(354))) { + return $error; + } + + if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { + return $result; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Send the SEND FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function sendFrom($path) + { + if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for sendFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function send_from($path) + { + return sendFrom($path); + } + + /** + * Send the SOML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function somlFrom($path) + { + if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for somlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function soml_from($path) + { + return somlFrom($path); + } + + /** + * Send the SAML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function samlFrom($path) + { + if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for samlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function saml_from($path) + { + return samlFrom($path); + } + + /** + * Send the RSET command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function rset() + { + if (PEAR::isError($error = $this->_put('RSET'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Send the VRFY command. + * + * @param string The string to verify + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function vrfy($string) + { + /* Note: 251 is also a valid response code */ + if (PEAR::isError($error = $this->_put('VRFY', $string))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) { + return $error; + } + + return true; + } + + /** + * Send the NOOP command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function noop() + { + if (PEAR::isError($error = $this->_put('NOOP'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility method. identifySender()'s functionality is + * now handled internally. + * + * @return boolean This method always return true. + * + * @access public + * @since 1.0 + */ + function identifySender() + { + return true; + } + +} diff --git a/Net/Socket.php b/Net/Socket.php new file mode 100644 index 0000000..d7a0495 --- /dev/null +++ b/Net/Socket.php @@ -0,0 +1,576 @@ + | +// | Chuck Hagenbuch | +// +----------------------------------------------------------------------+ +// +// $Id: Socket.php,v 1.31 2007/05/04 04:30:29 chagenbu Exp $ + +require_once 'PEAR.php'; + +define('NET_SOCKET_READ', 1); +define('NET_SOCKET_WRITE', 2); +define('NET_SOCKET_ERROR', 4); + +/** + * Generalized Socket class. + * + * @version 1.1 + * @author Stig Bakken + * @author Chuck Hagenbuch + */ +class Net_Socket extends PEAR { + + /** + * Socket file pointer. + * @var resource $fp + */ + var $fp = null; + + /** + * Whether the socket is blocking. Defaults to true. + * @var boolean $blocking + */ + var $blocking = true; + + /** + * Whether the socket is persistent. Defaults to false. + * @var boolean $persistent + */ + var $persistent = false; + + /** + * The IP address to connect to. + * @var string $addr + */ + var $addr = ''; + + /** + * The port number to connect to. + * @var integer $port + */ + var $port = 0; + + /** + * Number of seconds to wait on socket connections before assuming + * there's no more data. Defaults to no timeout. + * @var integer $timeout + */ + var $timeout = false; + + /** + * Number of bytes to read at a time in readLine() and + * readAll(). Defaults to 2048. + * @var integer $lineLength + */ + var $lineLength = 2048; + + /** + * Connect to the specified port. If called when the socket is + * already connected, it disconnects and connects again. + * + * @param string $addr IP address or host name. + * @param integer $port TCP port number. + * @param boolean $persistent (optional) Whether the connection is + * persistent (kept open between requests + * by the web server). + * @param integer $timeout (optional) How long to wait for data. + * @param array $options See options for stream_context_create. + * + * @access public + * + * @return boolean | PEAR_Error True on success or a PEAR_Error on failure. + */ + function connect($addr, $port = 0, $persistent = null, $timeout = null, $options = null) + { + if (is_resource($this->fp)) { + @fclose($this->fp); + $this->fp = null; + } + + if (!$addr) { + return $this->raiseError('$addr cannot be empty'); + } elseif (strspn($addr, '.0123456789') == strlen($addr) || + strstr($addr, '/') !== false) { + $this->addr = $addr; + } else { + $this->addr = @gethostbyname($addr); + } + + $this->port = $port % 65536; + + if ($persistent !== null) { + $this->persistent = $persistent; + } + + if ($timeout !== null) { + $this->timeout = $timeout; + } + + $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen'; + $errno = 0; + $errstr = ''; + if ($options && function_exists('stream_context_create')) { + if ($this->timeout) { + $timeout = $this->timeout; + } else { + $timeout = 0; + } + $context = stream_context_create($options); + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout, $context); + } else { + if ($this->timeout) { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $this->timeout); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr); + } + } + + if (!$fp) { + return $this->raiseError($errstr, $errno); + } + + $this->fp = $fp; + + return $this->setBlocking($this->blocking); + } + + /** + * Disconnects from the peer, closes the socket. + * + * @access public + * @return mixed true on success or an error object otherwise + */ + function disconnect() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + @fclose($this->fp); + $this->fp = null; + return true; + } + + /** + * Find out if the socket is in blocking mode. + * + * @access public + * @return boolean The current blocking mode. + */ + function isBlocking() + { + return $this->blocking; + } + + /** + * Sets whether the socket connection should be blocking or + * not. A read call to a non-blocking socket will return immediately + * if there is no data available, whereas it will block until there + * is data for blocking sockets. + * + * @param boolean $mode True for blocking sockets, false for nonblocking. + * @access public + * @return mixed true on success or an error object otherwise + */ + function setBlocking($mode) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $this->blocking = $mode; + socket_set_blocking($this->fp, $this->blocking); + return true; + } + + /** + * Sets the timeout value on socket descriptor, + * expressed in the sum of seconds and microseconds + * + * @param integer $seconds Seconds. + * @param integer $microseconds Microseconds. + * @access public + * @return mixed true on success or an error object otherwise + */ + function setTimeout($seconds, $microseconds) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_set_timeout($this->fp, $seconds, $microseconds); + } + + /** + * Sets the file buffering size on the stream. + * See php's stream_set_write_buffer for more information. + * + * @param integer $size Write buffer size. + * @access public + * @return mixed on success or an PEAR_Error object otherwise + */ + function setWriteBuffer($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $returned = stream_set_write_buffer($this->fp, $code); + if ($returned == 0) { + return true; + } + return $this->raiseError('Cannot set write buffer.'); + } + + /** + * Returns information about an existing socket resource. + * Currently returns four entries in the result array: + * + *

+ * timed_out (bool) - The socket timed out waiting for data
+ * blocked (bool) - The socket was blocked
+ * eof (bool) - Indicates EOF event
+ * unread_bytes (int) - Number of bytes left in the socket buffer
+ *

+ * + * @access public + * @return mixed Array containing information about existing socket resource or an error object otherwise + */ + function getStatus() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_get_status($this->fp); + } + + /** + * Get a specified line of data + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function gets($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fgets($this->fp, $size); + } + + /** + * Read a specified amount of data. This is guaranteed to return, + * and has the added benefit of getting everything in one fread() + * chunk; if you know the size of the data you're getting + * beforehand, this is definitely the way to go. + * + * @param integer $size The number of bytes to read from the socket. + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function read($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fread($this->fp, $size); + } + + /** + * Write a specified amount of data. + * + * @param string $data Data to write. + * @param integer $blocksize Amount of data to write at once. + * NULL means all at once. + * + * @access public + * @return mixed true on success or an error object otherwise + */ + function write($data, $blocksize = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($blocksize) && !OS_WINDOWS) { + return fwrite($this->fp, $data); + } else { + if (is_null($blocksize)) { + $blocksize = 1024; + } + + $pos = 0; + $size = strlen($data); + while ($pos < $size) { + $written = @fwrite($this->fp, substr($data, $pos, $blocksize)); + if ($written === false) { + return false; + } + $pos += $written; + } + + return $pos; + } + } + + /** + * Write a line of data to the socket, followed by a trailing "\r\n". + * + * @access public + * @return mixed fputs result, or an error + */ + function writeLine($data) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return fwrite($this->fp, $data . "\r\n"); + } + + /** + * Tests for end-of-file on a socket descriptor. + * + * Also returns true if the socket is disconnected. + * + * @access public + * @return bool + */ + function eof() + { + return (!is_resource($this->fp) || feof($this->fp)); + } + + /** + * Reads a byte of data + * + * @access public + * @return 1 byte of data from the socket, or a PEAR_Error if + * not connected. + */ + function readByte() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return ord(@fread($this->fp, 1)); + } + + /** + * Reads a word of data + * + * @access public + * @return 1 word of data from the socket, or a PEAR_Error if + * not connected. + */ + function readWord() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 2); + return (ord($buf[0]) + (ord($buf[1]) << 8)); + } + + /** + * Reads an int of data + * + * @access public + * @return integer 1 int of data from the socket, or a PEAR_Error if + * not connected. + */ + function readInt() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return (ord($buf[0]) + (ord($buf[1]) << 8) + + (ord($buf[2]) << 16) + (ord($buf[3]) << 24)); + } + + /** + * Reads a zero-terminated string of data + * + * @access public + * @return string, or a PEAR_Error if + * not connected. + */ + function readString() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $string = ''; + while (($char = @fread($this->fp, 1)) != "\x00") { + $string .= $char; + } + return $string; + } + + /** + * Reads an IP Address and returns it in a dot formated string + * + * @access public + * @return Dot formated string, or a PEAR_Error if + * not connected. + */ + function readIPAddress() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return sprintf("%s.%s.%s.%s", ord($buf[0]), ord($buf[1]), + ord($buf[2]), ord($buf[3])); + } + + /** + * Read until either the end of the socket or a newline, whichever + * comes first. Strips the trailing newline from the returned data. + * + * @access public + * @return All available data up to a newline, without that + * newline, or until the end of the socket, or a PEAR_Error if + * not connected. + */ + function readLine() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $line = ''; + $timeout = time() + $this->timeout; + while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) { + $line .= @fgets($this->fp, $this->lineLength); + if (substr($line, -1) == "\n") { + return rtrim($line, "\r\n"); + } + } + return $line; + } + + /** + * Read until the socket closes, or until there is no more data in + * the inner PHP buffer. If the inner buffer is empty, in blocking + * mode we wait for at least 1 byte of data. Therefore, in + * blocking mode, if there is no data at all to be read, this + * function will never exit (unless the socket is closed on the + * remote end). + * + * @access public + * + * @return string All data until the socket closes, or a PEAR_Error if + * not connected. + */ + function readAll() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $data = ''; + while (!feof($this->fp)) { + $data .= @fread($this->fp, $this->lineLength); + } + return $data; + } + + /** + * Runs the equivalent of the select() system call on the socket + * with a timeout specified by tv_sec and tv_usec. + * + * @param integer $state Which of read/write/error to check for. + * @param integer $tv_sec Number of seconds for timeout. + * @param integer $tv_usec Number of microseconds for timeout. + * + * @access public + * @return False if select fails, integer describing which of read/write/error + * are ready, or PEAR_Error if not connected. + */ + function select($state, $tv_sec, $tv_usec = 0) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $read = null; + $write = null; + $except = null; + if ($state & NET_SOCKET_READ) { + $read[] = $this->fp; + } + if ($state & NET_SOCKET_WRITE) { + $write[] = $this->fp; + } + if ($state & NET_SOCKET_ERROR) { + $except[] = $this->fp; + } + if (false === ($sr = stream_select($read, $write, $except, $tv_sec, $tv_usec))) { + return false; + } + + $result = 0; + if (count($read)) { + $result |= NET_SOCKET_READ; + } + if (count($write)) { + $result |= NET_SOCKET_WRITE; + } + if (count($except)) { + $result |= NET_SOCKET_ERROR; + } + return $result; + } + + /** + * Turns encryption on/off on a connected socket. + * + * @param bool $enabled Set this parameter to true to enable encryption + * and false to disable encryption. + * @param integer $type Type of encryption. See + * http://se.php.net/manual/en/function.stream-socket-enable-crypto.php for values. + * + * @access public + * @return false on error, true on success and 0 if there isn't enough data and the + * user should try again (non-blocking sockets only). A PEAR_Error object + * is returned if the socket is not connected + */ + function enableCrypto($enabled, $type) + { + if (version_compare(phpversion(), "5.1.0", ">=")) { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + return @stream_socket_enable_crypto($this->fp, $enabled, $type); + } else { + return $this->raiseError('Net_Socket::enableCrypto() requires php version >= 5.1.0'); + } + } + +} diff --git a/Net/URL.php b/Net/URL.php new file mode 100644 index 0000000..3dcfef6 --- /dev/null +++ b/Net/URL.php @@ -0,0 +1,485 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: URL.php,v 1.49 2007/06/28 14:43:07 davidc Exp $ +// +// Net_URL Class + + +class Net_URL +{ + var $options = array('encode_query_keys' => false); + /** + * Full url + * @var string + */ + var $url; + + /** + * Protocol + * @var string + */ + var $protocol; + + /** + * Username + * @var string + */ + var $username; + + /** + * Password + * @var string + */ + var $password; + + /** + * Host + * @var string + */ + var $host; + + /** + * Port + * @var integer + */ + var $port; + + /** + * Path + * @var string + */ + var $path; + + /** + * Query string + * @var array + */ + var $querystring; + + /** + * Anchor + * @var string + */ + var $anchor; + + /** + * Whether to use [] + * @var bool + */ + var $useBrackets; + + /** + * PHP4 Constructor + * + * @see __construct() + */ + function Net_URL($url = null, $useBrackets = true) + { + $this->__construct($url, $useBrackets); + } + + /** + * PHP5 Constructor + * + * Parses the given url and stores the various parts + * Defaults are used in certain cases + * + * @param string $url Optional URL + * @param bool $useBrackets Whether to use square brackets when + * multiple querystrings with the same name + * exist + */ + function __construct($url = null, $useBrackets = true) + { + $this->url = $url; + $this->useBrackets = $useBrackets; + + $this->initialize(); + } + + function initialize() + { + $HTTP_SERVER_VARS = !empty($_SERVER) ? $_SERVER : $GLOBALS['HTTP_SERVER_VARS']; + + $this->user = ''; + $this->pass = ''; + $this->host = ''; + $this->port = 80; + $this->path = ''; + $this->querystring = array(); + $this->anchor = ''; + + // Only use defaults if not an absolute URL given + if (!preg_match('/^[a-z0-9]+:\/\//i', $this->url)) { + $this->protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http'); + + /** + * Figure out host/port + */ + if (!empty($HTTP_SERVER_VARS['HTTP_HOST']) && + preg_match('/^(.*)(:([0-9]+))?$/U', $HTTP_SERVER_VARS['HTTP_HOST'], $matches)) + { + $host = $matches[1]; + if (!empty($matches[3])) { + $port = $matches[3]; + } else { + $port = $this->getStandardPort($this->protocol); + } + } + + $this->user = ''; + $this->pass = ''; + $this->host = !empty($host) ? $host : (isset($HTTP_SERVER_VARS['SERVER_NAME']) ? $HTTP_SERVER_VARS['SERVER_NAME'] : 'localhost'); + $this->port = !empty($port) ? $port : (isset($HTTP_SERVER_VARS['SERVER_PORT']) ? $HTTP_SERVER_VARS['SERVER_PORT'] : $this->getStandardPort($this->protocol)); + $this->path = !empty($HTTP_SERVER_VARS['PHP_SELF']) ? $HTTP_SERVER_VARS['PHP_SELF'] : '/'; + $this->querystring = isset($HTTP_SERVER_VARS['QUERY_STRING']) ? $this->_parseRawQuerystring($HTTP_SERVER_VARS['QUERY_STRING']) : null; + $this->anchor = ''; + } + + // Parse the url and store the various parts + if (!empty($this->url)) { + $urlinfo = parse_url($this->url); + + // Default querystring + $this->querystring = array(); + + foreach ($urlinfo as $key => $value) { + switch ($key) { + case 'scheme': + $this->protocol = $value; + $this->port = $this->getStandardPort($value); + break; + + case 'user': + case 'pass': + case 'host': + case 'port': + $this->$key = $value; + break; + + case 'path': + if ($value{0} == '/') { + $this->path = $value; + } else { + $path = dirname($this->path) == DIRECTORY_SEPARATOR ? '' : dirname($this->path); + $this->path = sprintf('%s/%s', $path, $value); + } + break; + + case 'query': + $this->querystring = $this->_parseRawQueryString($value); + break; + + case 'fragment': + $this->anchor = $value; + break; + } + } + } + } + /** + * Returns full url + * + * @return string Full url + * @access public + */ + function getURL() + { + $querystring = $this->getQueryString(); + + $this->url = $this->protocol . '://' + . $this->user . (!empty($this->pass) ? ':' : '') + . $this->pass . (!empty($this->user) ? '@' : '') + . $this->host . ($this->port == $this->getStandardPort($this->protocol) ? '' : ':' . $this->port) + . $this->path + . (!empty($querystring) ? '?' . $querystring : '') + . (!empty($this->anchor) ? '#' . $this->anchor : ''); + + return $this->url; + } + + /** + * Adds or updates a querystring item (URL parameter). + * Automatically encodes parameters with rawurlencode() if $preencoded + * is false. + * You can pass an array to $value, it gets mapped via [] in the URL if + * $this->useBrackets is activated. + * + * @param string $name Name of item + * @param string $value Value of item + * @param bool $preencoded Whether value is urlencoded or not, default = not + * @access public + */ + function addQueryString($name, $value, $preencoded = false) + { + if ($this->getOption('encode_query_keys')) { + $name = rawurlencode($name); + } + + if ($preencoded) { + $this->querystring[$name] = $value; + } else { + $this->querystring[$name] = is_array($value) ? array_map('rawurlencode', $value): rawurlencode($value); + } + } + + /** + * Removes a querystring item + * + * @param string $name Name of item + * @access public + */ + function removeQueryString($name) + { + if ($this->getOption('encode_query_keys')) { + $name = rawurlencode($name); + } + + if (isset($this->querystring[$name])) { + unset($this->querystring[$name]); + } + } + + /** + * Sets the querystring to literally what you supply + * + * @param string $querystring The querystring data. Should be of the format foo=bar&x=y etc + * @access public + */ + function addRawQueryString($querystring) + { + $this->querystring = $this->_parseRawQueryString($querystring); + } + + /** + * Returns flat querystring + * + * @return string Querystring + * @access public + */ + function getQueryString() + { + if (!empty($this->querystring)) { + foreach ($this->querystring as $name => $value) { + // Encode var name + $name = rawurlencode($name); + + if (is_array($value)) { + foreach ($value as $k => $v) { + $querystring[] = $this->useBrackets ? sprintf('%s[%s]=%s', $name, $k, $v) : ($name . '=' . $v); + } + } elseif (!is_null($value)) { + $querystring[] = $name . '=' . $value; + } else { + $querystring[] = $name; + } + } + $querystring = implode(ini_get('arg_separator.output'), $querystring); + } else { + $querystring = ''; + } + + return $querystring; + } + + /** + * Parses raw querystring and returns an array of it + * + * @param string $querystring The querystring to parse + * @return array An array of the querystring data + * @access private + */ + function _parseRawQuerystring($querystring) + { + $parts = preg_split('/[' . preg_quote(ini_get('arg_separator.input'), '/') . ']/', $querystring, -1, PREG_SPLIT_NO_EMPTY); + $return = array(); + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + $value = substr($part, strpos($part, '=') + 1); + $key = substr($part, 0, strpos($part, '=')); + } else { + $value = null; + $key = $part; + } + + if (!$this->getOption('encode_query_keys')) { + $key = rawurldecode($key); + } + + if (preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) { + $key = $matches[1]; + $idx = $matches[2]; + + // Ensure is an array + if (empty($return[$key]) || !is_array($return[$key])) { + $return[$key] = array(); + } + + // Add data + if ($idx === '') { + $return[$key][] = $value; + } else { + $return[$key][$idx] = $value; + } + } elseif (!$this->useBrackets AND !empty($return[$key])) { + $return[$key] = (array)$return[$key]; + $return[$key][] = $value; + } else { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Resolves //, ../ and ./ from a path and returns + * the result. Eg: + * + * /foo/bar/../boo.php => /foo/boo.php + * /foo/bar/../../boo.php => /boo.php + * /foo/bar/.././/boo.php => /foo/boo.php + * + * This method can also be called statically. + * + * @param string $path URL path to resolve + * @return string The result + */ + function resolvePath($path) + { + $path = explode('/', str_replace('//', '/', $path)); + + for ($i=0; $i 1 OR ($i == 1 AND $path[0] != '') ) ) { + unset($path[$i]); + unset($path[$i-1]); + $path = array_values($path); + $i -= 2; + + } elseif ($path[$i] == '..' AND $i == 1 AND $path[0] == '') { + unset($path[$i]); + $path = array_values($path); + $i--; + + } else { + continue; + } + } + + return implode('/', $path); + } + + /** + * Returns the standard port number for a protocol + * + * @param string $scheme The protocol to lookup + * @return integer Port number or NULL if no scheme matches + * + * @author Philippe Jausions + */ + function getStandardPort($scheme) + { + switch (strtolower($scheme)) { + case 'http': return 80; + case 'https': return 443; + case 'ftp': return 21; + case 'imap': return 143; + case 'imaps': return 993; + case 'pop3': return 110; + case 'pop3s': return 995; + default: return null; + } + } + + /** + * Forces the URL to a particular protocol + * + * @param string $protocol Protocol to force the URL to + * @param integer $port Optional port (standard port is used by default) + */ + function setProtocol($protocol, $port = null) + { + $this->protocol = $protocol; + $this->port = is_null($port) ? $this->getStandardPort($protocol) : $port; + } + + /** + * Set an option + * + * This function set an option + * to be used thorough the script. + * + * @access public + * @param string $optionName The optionname to set + * @param string $value The value of this option. + */ + function setOption($optionName, $value) + { + if (!array_key_exists($optionName, $this->options)) { + return false; + } + + $this->options[$optionName] = $value; + $this->initialize(); + } + + /** + * Get an option + * + * This function gets an option + * from the $this->options array + * and return it's value. + * + * @access public + * @param string $opionName The name of the option to retrieve + * @see $this->options + */ + function getOption($optionName) + { + if (!isset($this->options[$optionName])) { + return false; + } + + return $this->options[$optionName]; + } + +} +?> diff --git a/Numbers/Words.php b/Numbers/Words.php new file mode 100644 index 0000000..795ba0a --- /dev/null +++ b/Numbers/Words.php @@ -0,0 +1,188 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Words.php,v 1.4 2006/06/13 11:29:35 makler Exp $ +// + +/** + * The Numbers_Words class provides method to convert arabic numerals to + * words (also with currency name). + * + * @author Piotr Klaban + * @package Numbers_Words + */ + +// {{{ Numbers_Words + +/** + * The Numbers_Words class provides method to convert arabic numerals to words. + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + * @package Numbers_Words + */ +class Numbers_Words +{ + // {{{ toWords() + + /** + * Converts a number to its word representation + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that should be converted to a words representation + * + * @param string $locale Language name abbreviation. Optional. Defaults to en_US. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $locale = 'en_US') { + + include_once("Numbers/Words/lang.${locale}.php"); + + $classname = "Numbers_Words_${locale}"; + + if (!class_exists($classname)) { + return Numbers_Words::raiseError("Unable to include the Numbers/Words/lang.${locale}.php file"); + } + + $methods = get_class_methods($classname); + + if (!in_array('toWords', $methods) && !in_array('towords', $methods)) { + return Numbers_Words::raiseError("Unable to find toWords method in '$classname' class"); + } + + @$obj =& new $classname; + + return trim($obj->toWords($num)); + } + // }}} + // {{{ toCurrency() + /** + * Converts a currency value to word representation (1.02 => one dollar two cents) + * If the number has not any fraction part, the "cents" number is omitted. + * + * @param float $num A float/integer/string number representing currency value + * + * @param string $locale Language name abbreviation. Optional. Defaults to en_US. + * + * @param string $int_curr International currency symbol + * as defined by the ISO 4217 standard (three characters). + * E.g. 'EUR', 'USD', 'PLN'. Optional. + * Defaults to $def_currency defined in the language class. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toCurrency($num, $locale = 'en_US', $int_curr = '') { + $ret = $num; + + @include_once("Numbers/Words/lang.${locale}.php"); + + $classname = "Numbers_Words_${locale}"; + + if (!class_exists($classname)) { + return Numbers_Words::raiseError("Unable to include the Numbers/Words/lang.${locale}.php file"); + } + + $methods = get_class_methods($classname); + + if (!in_array('toCurrencyWords', $methods) && !in_array('tocurrencywords', $methods)) { + return Numbers_Words::raiseError("Unable to find toCurrencyWords method in '$classname' class"); + } + + @$obj =& new $classname; + + if (strpos($num, '.') === false) + { + $ret = trim($obj->toCurrencyWords($int_curr, $num)); + } else { + $currency = explode('.', $num, 2); + /* add leading zero */ + if (strlen($currency[1]) == 1) { + $currency[1] .= '0'; + } + $ret = trim($obj->toCurrencyWords($int_curr, $currency[0], $currency[1])); + } + return $ret; + } + // }}} + // {{{ getLocales() + /** + * Lists available locales for Numbers_Words + * + * @param string $int_curr International currency symbol + * @param mixed string/array of strings $locale + * Optional searched language name abbreviation. + * Default: all available locales. + * + * @return array The available locales (optionaly only the requested ones) + * @author Piotr Klaban + * @author Bertrand Gugger, bertrand at toggg dot com + * + * @access public + * @static + */ + function getLocales($locale = null) { + $ret = array(); + if (isset($locale) && is_string($locale)) { + $locale = array($locale); + } + $dname = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Words' . DIRECTORY_SEPARATOR; + $dh=opendir($dname); + if ($dh) { + while ($fname = readdir($dh)) { + if (preg_match('#^lang\.([a-z_]+)\.php$#i', $fname, $matches)) { + if (is_file($dname . $fname) && is_readable($dname . $fname) && + (!isset($locale) || in_array($matches[1], $locale))) { + $ret[] = $matches[1]; + } + } + } + closedir($dh); + sort($ret); + } + return $ret; + } + // }}} + // {{{ raiseError() + /** + * Trigger a PEAR error + * + * To improve performances, the PEAR.php file is included dynamically. + * + * @param string error message + */ + function raiseError($msg) + { + include_once('PEAR.php'); + return PEAR::raiseError($msg); + } + // }}} +} + +// }}} +?> diff --git a/Numbers/Words/lang.bg.php b/Numbers/Words/lang.bg.php new file mode 100644 index 0000000..7d1f171 --- /dev/null +++ b/Numbers/Words/lang.bg.php @@ -0,0 +1,505 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.bg.php,v 1.4 2004/10/22 18:22:52 kouber Exp $ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Bulgarian. + * + * @author Kouber Saparev + * @package Numbers_Words + */ +class Numbers_Words_bg extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name. + * @var string + * @access public + */ + var $locale = 'bg'; + + /** + * Language name in English. + * @var string + * @access public + */ + var $lang = 'Bulgarian'; + + /** + * Native language name. + * @var string + * @access public + */ + var $lang_native = 'Áúëãàðñêè'; + + /** + * Some miscellaneous words and language constructs. + * @var string + * @access private + */ + var $_misc_strings = array( + 'deset'=>'äåñåò', // "ten" + 'edinadeset'=>'åäèíàäåñåò', // "eleven" + 'na'=>'íà', // liaison particle for 12 to 19 + 'sto'=>'ñòî', // "hundred" + 'sta'=>'ñòà', // suffix for 2 and 3 hundred + 'stotin'=>'ñòîòèí', // suffix for 4 to 9 hundred + 'hiliadi'=>'õèëÿäè' // plural form of "thousand" + ); + + + /** + * The words for digits (except zero). Note that, there are three genders for them (neuter, masculine and feminine). + * The words for 3 to 9 (masculine) and for 2 to 9 (feminine) are the same as neuter, so they're filled + * in the _initDigits() method, which is invoked from the constructor. + * @var string + * @access private + */ + var $_digits = array( + 0=>array(1=>"åäíî", "äâå", "òðè", "÷åòèðè", "ïåò", "øåñò", "ñåäåì", "îñåì", "äåâåò"), // neuter + 1=>array(1=>'åäèí', 'äâà'), // masculine + -1=>array(1=>'åäíà') // feminine + ); + + /** + * A flag, that determines if the _digits array is filled for masculine and feminine genders. + * @var string + * @access private + */ + var $_digits_initialized = false; + + /** + * A flag, that determines if the "and" word is placed already before the last non-empty group of digits. + * @var string + * @access private + */ + var $_last_and = false; + + /** + * The word for zero. + * @var string + * @access private + */ + var $_zero = 'íóëà'; + + /** + * The word for infinity. + * @var string + * @access private + */ + var $_infinity = 'áåçêðàéíîñò'; + + /** + * The word for the "and" language construct. + * @var string + * @access private + */ + var $_and = 'è'; + + /** + * The word separator. + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The word for the minus sign. + * @var string + * @access private + */ + var $_minus = 'ìèíóñ'; // minus sign + + /** + * The plural suffix (except for thousand). + * @var string + * @access private + */ + var $_plural = 'à'; // plural suffix + + /** + * The suffixes for exponents (singular). + * @var array + * @access private + */ + var $_exponent = array( + 0 => '', + 3 => 'õèëÿäà', + 6 => 'ìèëèîí', + 9 => 'ìèëèàðä', + 12 => 'òðèëèîí', + 15 => 'êâàäðèëèîí', + 18 => 'êâèíòèëèîí', + 21 => 'ñåêñòèëèîí', + 24 => 'ñåïòèëèîí', + 27 => 'îêòèëèîí', + 30 => 'íîíàëèîí', + 33 => 'äåêàëèîí', + 36 => 'óíäåêàëèîí', + 39 => 'äóîäåêàëèîí', + 42 => 'òðåäåêàëèîí', + 45 => 'êâàòîðäåêàëèîí', + 48 => 'êâèíòäåêàëèîí', + 51 => 'ñåêñäåêàëèîí', + 54 => 'ñåïòäåêàëèîí', + 57 => 'îêòîäåêàëèîí', + 60 => 'íîâåìäåêàëèîí', + 63 => 'âèãèíòèëèîí', + 66 => 'óíâèãèíòèëèîí', + 69 => 'äóîâèãèíòèëèîí', + 72 => 'òðåâèãèíòèëèîí', + 75 => 'êâàòîðâèãèíòèëèîí', + 78 => 'êâèíâèãèíòèëèîí', + 81 => 'ñåêñâèãèíòèëèîí', + 84 => 'ñåïòåíâèãèíòèëèîí', + 87 => 'îêòîâèãèíòèëèîí', + 90 => 'íîâåìâèãèíòèëèîí', + 93 => 'òðèãèíòèëèîí', + 96 => 'óíòðèãèíòèëèîí', + 99 => 'äóîòðèãèíòèëèîí', + 102 => 'òðåòðèãèíòèëèîí', + 105 => 'êâàòîðòðèãèíòèëèîí', + 108 => 'êâèíòðèãèíòèëèîí', + 111 => 'ñåêñòðèãèíòèëèîí', + 114 => 'ñåïòåíòðèãèíòèëèîí', + 117 => 'îêòîòðèãèíòèëèîí', + 120 => 'íîâåìòðèãèíòèëèîí', + 123 => 'êâàäðàãèíòèëèîí', + 126 => 'óíêâàäðàãèíòèëèîí', + 129 => 'äóîêâàäðàãèíòèëèîí', + 132 => 'òðåêâàäðàãèíòèëèîí', + 135 => 'êâàòîðêâàäðàãèíòèëèîí', + 138 => 'êâèíêâàäðàãèíòèëèîí', + 141 => 'ñåêñêâàäðàãèíòèëèîí', + 144 => 'ñåïòåíêâàäðàãèíòèëèîí', + 147 => 'îêòîêâàäðàãèíòèëèîí', + 150 => 'íîâåìêâàäðàãèíòèëèîí', + 153 => 'êâèíêâàãèíòèëèîí', + 156 => 'óíêâèíêàãèíòèëèîí', + 159 => 'äóîêâèíêàãèíòèëèîí', + 162 => 'òðåêâèíêàãèíòèëèîí', + 165 => 'êâàòîðêâèíêàãèíòèëèîí', + 168 => 'êâèíêâèíêàãèíòèëèîí', + 171 => 'ñåêñêâèíêàãèíòèëèîí', + 174 => 'ñåïòåíêâèíêàãèíòèëèîí', + 177 => 'îêòîêâèíêàãèíòèëèîí', + 180 => 'íîâåìêâèíêàãèíòèëèîí', + 183 => 'ñåêñàãèíòèëèîí', + 186 => 'óíñåêñàãèíòèëèîí', + 189 => 'äóîñåêñàãèíòèëèîí', + 192 => 'òðåñåêñàãèíòèëèîí', + 195 => 'êâàòîðñåêñàãèíòèëèîí', + 198 => 'êâèíñåêñàãèíòèëèîí', + 201 => 'ñåêññåêñàãèíòèëèîí', + 204 => 'ñåïòåíñåêñàãèíòèëèîí', + 207 => 'îêòîñåêñàãèíòèëèîí', + 210 => 'íîâåìñåêñàãèíòèëèîí', + 213 => 'ñåïòàãèíòèëèîí', + 216 => 'óíñåïòàãèíòèëèîí', + 219 => 'äóîñåïòàãèíòèëèîí', + 222 => 'òðåñåïòàãèíòèëèîí', + 225 => 'êâàòîðñåïòàãèíòèëèîí', + 228 => 'êâèíñåïòàãèíòèëèîí', + 231 => 'ñåêññåïòàãèíòèëèîí', + 234 => 'ñåïòåíñåïòàãèíòèëèîí', + 237 => 'îêòîñåïòàãèíòèëèîí', + 240 => 'íîâåìñåïòàãèíòèëèîí', + 243 => 'îêòîãèíòèëèîí', + 246 => 'óíîêòîãèíòèëèîí', + 249 => 'äóîîêòîãèíòèëèîí', + 252 => 'òðåîêòîãèíòèëèîí', + 255 => 'êâàòîðîêòîãèíòèëèîí', + 258 => 'êâèíîêòîãèíòèëèîí', + 261 => 'ñåêñîêòîãèíòèëèîí', + 264 => 'ñåïòîêòîãèíòèëèîí', + 267 => 'îêòîîêòîãèíòèëèîí', + 270 => 'íîâåìîêòîãèíòèëèîí', + 273 => 'íîíàãèíòèëèîí', + 276 => 'óííîíàãèíòèëèîí', + 279 => 'äóîíîíàãèíòèëèîí', + 282 => 'òðåíîíàãèíòèëèîí', + 285 => 'êâàòîðíîíàãèíòèëèîí', + 288 => 'êâèííîíàãèíòèëèîí', + 291 => 'ñåêñíîíàãèíòèëèîí', + 294 => 'ñåïòåííîíàãèíòèëèîí', + 297 => 'îêòîíîíàãèíòèëèîí', + 300 => 'íîâåìíîíàãèíòèëèîí', + 303 => 'öåíòèëèîí' + ); + // }}} + + // {{{ Numbers_Words_bg() + + /** + * The class constructor, used for calling the _initDigits method. + * + * @return void + * + * @access public + * @author Kouber Saparev + * @see function _initDigits + */ + function Numbers_Words_bg() { + $this->_initDigits(); + } + // }}} + + // {{{ _initDigits() + + /** + * Fills the _digits array for masculine and feminine genders with + * corresponding references to neuter words (when they're the same). + * + * @return void + * + * @access private + * @author Kouber Saparev + */ + function _initDigits() { + if (!$this->_digits_initialized) { + for ($i=3; $i<=9; $i++) { + $this->_digits[1][$i] =& $this->_digits[0][$i]; + } + for ($i=2; $i<=9; $i++) { + $this->_digits[-1][$i] =& $this->_digits[0][$i]; + } + $this->_digits_initialized = true; + } + } + // }}} + + // {{{ _splitNumber() + + /** + * Split a number to groups of three-digit numbers. + * + * @param mixed $num An integer or its string representation + * that need to be split + * + * @return array Groups of three-digit numbers. + * + * @access private + * @author Kouber Saparev + * @since PHP 4.2.3 + */ + + function _splitNumber($num) + { + if (is_string($num)) { + $ret = array(); + $strlen = strlen($num); + $first = substr($num, 0, $strlen%3); + preg_match_all('/\d{3}/', substr($num, $strlen%3, $strlen), $m); + $ret =& $m[0]; + if ($first) array_unshift($ret, $first); + return $ret; + } + else + return explode(' ', number_format($num, 0, '', ' ')); // a faster version for integers + } + // }}} + + // {{{ _showDigitsGroup() + + /** + * Converts a three-digit number to its word representation + * in Bulgarian language. + * + * @param integer $num An integer between 1 and 999 inclusive. + * + * @param integer $gender An integer which represents the gender of + * the current digits group. + * 0 - neuter + * 1 - masculine + * -1 - feminine + * + * @param boolean $last A flag that determines if the current digits group + * is the last one. + * + * @return string The words for the given number. + * + * @access private + * @author Kouber Saparev + */ + function _showDigitsGroup($num, $gender = 0, $last = false) + { + /* A storage array for the return string. + Positions 1, 3, 5 are intended for digit words + and everything else (0, 2, 4) for "and" words. + Both of the above types are optional, so the size of + the array may vary. + */ + $ret = array(); + + // extract the value of each digit from the three-digit number + $e = $num%10; // ones + $d = ($num-$e)%100/10; // tens + $s = ($num-$d*10-$e)%1000/100; // hundreds + + // process the "hundreds" digit. + if ($s) { + switch ($s) { + case 1: + $ret[1] = $this->_misc_strings['sto']; + break; + case 2: + case 3: + $ret[1] = $this->_digits[0][$s].$this->_misc_strings['sta']; + break; + default: + $ret[1] = $this->_digits[0][$s].$this->_misc_strings['stotin']; + } + } + + // process the "tens" digit, and optionally the "ones" digit. + if ($d) { + // in the case of 1, the "ones" digit also must be processed + if ($d==1) { + if (!$e) { + $ret[3] = $this->_misc_strings['deset']; // ten + } else { + if ($e==1) { + $ret[3] = $this->_misc_strings['edinadeset']; // eleven + } else { + $ret[3] = $this->_digits[1][$e].$this->_misc_strings['na'].$this->_misc_strings['deset']; // twelve - nineteen + } + // the "ones" digit is alredy processed, so skip a second processment + $e = 0; + } + } else { + $ret[3] = $this->_digits[1][$d].$this->_misc_strings['deset']; // twenty - ninety + } + } + + // process the "ones" digit + if ($e) { + $ret[5] = $this->_digits[$gender][$e]; + } + + // put "and" where needed + if (count($ret)>1) { + if ($e) { + $ret[4] = $this->_and; + } else { + $ret[2] = $this->_and; + } + } + + // put "and" optionally in the case this is the last non-empty group + if ($last) { + if (!$s||count($ret)==1) { + $ret[0] = $this->_and; + } + $this->_last_and = true; + } + + // sort the return array so that "and" constructs go to theirs appropriate places + ksort($ret); + + return implode($this->_sep, $ret); + } + // }}} + + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Bulgarian language. + * + * @param integer $num An integer between 9.99*-10^302 and 9.99*10^302 (999 centillions) + * that need to be converted to words + * + * @return string The corresponding word representation + * + * @access public + * @author Kouber Saparev + */ + function toWords($num = 0) + { + $ret = array(); + $ret_minus = ''; + + // check if $num is a valid non-zero number + if (!$num || preg_match('/^-?0+$/', $num) || !preg_match('/^-?\d+$/', $num)) return $this->_zero; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret_minus = $this->_minus . $this->_sep; + $num = substr($num, 1); + } + + // if the absolute value is greater than 9.99*10^302, return infinity + if (strlen($num)>306) { + return $ret_minus . $this->_infinity; + } + + // strip excessive zero signs + $num = ltrim($num, '0'); + + // split $num to groups of three-digit numbers + $num_groups = $this->_splitNumber($num); + + $sizeof_numgroups = count($num_groups); + + // go through the groups in reverse order, so that the last group could be determined + for ($i=$sizeof_numgroups-1, $j=1; $i>=0; $i--, $j++) { + if (!isset($ret[$j])) { + $ret[$j] = ''; + } + + // what is the corresponding exponent for the current group + $pow = $sizeof_numgroups-$i; + + // skip processment for empty groups + if ($num_groups[$i]!='000') { + if ($num_groups[$i]>1) { + if ($pow==1) { + $ret[$j] .= $this->_showDigitsGroup($num_groups[$i], 0, !$this->_last_and && $i).$this->_sep; + $ret[$j] .= $this->_exponent[($pow-1)*3]; + } elseif ($pow==2) { + $ret[$j] .= $this->_showDigitsGroup($num_groups[$i], -1, !$this->_last_and && $i).$this->_sep; + $ret[$j] .= $this->_misc_strings['hiliadi'].$this->_sep; + } else { + $ret[$j] .= $this->_showDigitsGroup($num_groups[$i], 1, !$this->_last_and && $i).$this->_sep; + $ret[$j] .= $this->_exponent[($pow-1)*3].$this->_plural.$this->_sep; + } + } else { + if ($pow==1) { + $ret[$j] .= $this->_showDigitsGroup($num_groups[$i], 0, !$this->_last_and && $i).$this->_sep; + } elseif ($pow==2) { + $ret[$j] .= $this->_exponent[($pow-1)*3].$this->_sep; + } else { + $ret[$j] .= $this->_digits[1][1].$this->_sep.$this->_exponent[($pow-1)*3].$this->_sep; + } + } + } + } + + return $ret_minus . rtrim(implode('', array_reverse($ret)), $this->_sep); + } + // }}} +} +?> diff --git a/Numbers/Words/lang.cs.php b/Numbers/Words/lang.cs.php new file mode 100644 index 0000000..6011912 --- /dev/null +++ b/Numbers/Words/lang.cs.php @@ -0,0 +1,339 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.cs.php,v 1.1 2005/02/28 13:22:51 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Czech language. +// + +/** + * Class for translating numbers into Czech. + * + * @author Petr 'PePa' Pavel + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Czech. + * + * @author Petr 'PePa' Pavel + * @package Numbers_Words + */ +class Numbers_Words_cs extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'cs'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Czech'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Czech'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'mínus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://cs.wikipedia.org/wiki/P%C5%99edpony_soustavy_SI + * the rest was translated from lang.en_GB.php + * names verified by "Ustav pro jazyk cesky" only up to Septilion + * (they could verify only the lingual matter - not the mathematical one) + * @var array + * @access private + */ + + var $_exponent = array( + 0 => array(''), + 3 => array('tisíc','tisíce','tisíc'), + 6 => array('milion','miliony','milionù'), + 9 => array('miliarda','miliardy','miliard'), + 12 => array('bilion','biliony','bilionù'), + 15 => array('biliarda','biliardy','biliard'), + 18 => array('trilion','triliony','trilionù'), + 21 => array('triliarda','triliardy','triliard'), + + 24 => array('kvadrilion','kvadriliony','kvadrilionù'), + 30 => array('kvintilion','kvintiliony','kvintilionù'), + 36 => array('sextilion','sextiliony','sextilionù'), + 42 => array('septilion','septiliony','septilionù'), + + 48 => array('oktilion','oktiliony','oktilionù'), + 54 => array('nonilion','noniliony','nonilionù'), + 60 => array('decilion','deciliony','decilionù'), + + 66 => array('undecilion','undeciliony','undecilionù'), + 72 => array('duodecilion','duodeciliony','duodecilionù'), + 78 => array('tredecilion','tredeciliony','tredecilionù'), + 84 => array('kvatrodecilion','kvatrodeciliony','kvatrodecilionù'), + 90 => array('kvindecilion','kvindeciliony','kvindecilionù'), + 96 => array('sexdecilion','sexdeciliony','sexdecilionù'), + 102 => array('septendecilion','septendeciliony','septendecilionù'), + 108 => array('oktodecilion','oktodeciliony','oktodecilionù'), + 114 => array('novemdecilion','novemdeciliony','novemdecilionù'), + 120 => array('vigintilion','vigintiliony','vigintilionù'), + 192 => array('duotrigintilion','duotrigintiliony','duotrigintilionù'), + 600 => array('centilion','centiliony','centilionù') + + ); + + /** + * The array containing the forms of Czech word for "hundred" + * @var array + * @access private + */ + var $_hundreds = array( + 0 => 'sto', 'stì', 'sta', 'set' + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'nula', 'jedna', 'dva', 'tøi', 'ètyøi', + 'pìt', '¹est', 'sedm', 'osm', 'devìt' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Czech language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Petr 'PePa' Pavel + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { +// print "
$num,$power,$powsuffix
"; + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + + // inflection of the word "hundred" + if ($h == 1) + $ret .= $this->_sep . $this->_hundreds[0]; + elseif ($h == 2) + $ret .= $this->_sep . "dvì" . $this->_sep . $this->_hundreds[1]; + elseif ( ($h > 1) && ($h < 5) ) + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . $this->_hundreds[2]; + else //if ($h >= 5) + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . $this->_hundreds[3]; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + switch ($t) { + case 2: + case 3: + case 4: + $ret .= $this->_sep . $this->_digits[$t] . 'cet'; + break; + + case 5: + $ret .= $this->_sep . 'padesát'; + break; + + case 6: + $ret .= $this->_sep . '¹edesát'; + break; + + case 7: + $ret .= $this->_sep . 'sedmdesát'; + break; + + case 8: + $ret .= $this->_sep . 'osmdesát'; + break; + + case 9: + $ret .= $this->_sep . 'devadesát'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'deset'; + break; + + case 1: + $ret .= $this->_sep . 'jedenáct'; + break; + + case 4: + $ret .= $this->_sep . 'ètrnáct'; + break; + + case 5: + $ret .= $this->_sep . 'patnáct'; + break; + + case 9: + $ret .= $this->_sep . 'devatenáct'; + break; + + case 2: + case 3: + case 6: + case 7: + case 8: + $ret .= $this->_sep . $this->_digits[$d] . 'náct'; + break; + } + break; + } + + if ($t != 1 && $d > 0 && ($power == 0 || $d > 1)) { + // + $ret .= $this->_sep . $this->_digits[$d]; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + // inflection of exponental words + if ($num == 1) + $idx = 0; + elseif ( ($num > 1) && ($num < 5) ) + $idx = 1; + else //if ($num >= 5) + $idx = 2; + + $ret .= $this->_sep . $lev[$idx]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.de.php b/Numbers/Words/lang.de.php new file mode 100644 index 0000000..aef8a99 --- /dev/null +++ b/Numbers/Words/lang.de.php @@ -0,0 +1,321 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.de.php,v 1.3 2004/09/30 07:44:33 makler Exp $ +// +// Numbers_Words class extension to spell numbers in German language. +// + +/** + * + * Class for translating numbers into German. + * @author Piotr Klaban + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into German. + * + * @author Piotr Klaban + * @package Numbers_Words + */ +class Numbers_Words_de extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'de'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'German'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Deutsch'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'Minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://german.about.com/library/blzahlenaud.htm + * http://www3.osk.3web.ne.jp/~nagatani/common/zahlwort.htm + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('tausend','tausend'), + 6 => array('Million','Millionen'), + 9 => array('Milliarde','Milliarden'), + 12 => array('Billion','Billionen'), + 15 => array('Billiarde','Billiarden'), + 18 => array('Trillion','Trillionen'), + 21 => array('Trilliarde','Trilliarden'), + 24 => array('Quadrillion','Quadrillionen'), + 27 => array('Quadrilliarde','Quadrilliarden'), + 30 => array('Quintillion','Quintillionen'), + 33 => array('Quintilliarde','Quintilliarden'), + 36 => array('Sextillion','Sextillionen'), + 39 => array('Sextilliarde','Sextilliarden'), + 42 => array('Septillion','Septillionen'), + 45 => array('Septilliarde','Septilliarden'), + 48 => array('Oktillion','Oktillionen'), // oder Octillionen + 51 => array('Oktilliarde','Oktilliarden'), + 54 => array('Nonillion','Nonillionen'), + 57 => array('Nonilliarde','Nonilliarden'), + 60 => array('Dezillion','Dezillionen'), + 63 => array('Dezilliarde','Dezilliarden'), + 120 => array('Vigintillion','Vigintillionen'), + 123 => array('Vigintilliarde','Vigintilliarden'), + 600 => array('Zentillion','Zentillionen'), // oder Centillion + 603 => array('Zentilliarde','Zentilliarden') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'null', 'ein', 'zwei', 'drei', 'vier', + 'fünf', 'sechs', 'sieben', 'acht', 'neun' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ''; + + /** + * The exponent word separator + * @var string + * @access private + */ + var $_sep2 = ' '; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in German language. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access private + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundert'; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + if ($t > 0) { + $ret .= $this->_digits[$d] . 'und'; + } else { + $ret .= $this->_digits[$d]; + if ($d == 1) + if ($power == 0) { + $ret .= 's'; // fuer eins + } else { + if ($power != 3) { // tausend ausnehmen + $ret .= 'e'; // fuer eine + } + } + } + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 8: + case 5: + $ret .= $this->_sep . $this->_digits[$t] . 'zig'; + break; + + case 7: + $ret .= $this->_sep . 'siebzig'; + break; + + case 6: + $ret .= $this->_sep . 'sechzig'; + break; + + case 4: + $ret .= $this->_sep . 'vierzig'; + break; + + case 3: + $ret .= $this->_sep . 'dreißig'; + break; + + case 2: + $ret .= $this->_sep . 'zwanzig'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'zehn'; + break; + + case 1: + $ret .= $this->_sep . 'elf'; + break; + + case 2: + $ret .= $this->_sep . 'zwölf'; + break; + + case 3: + case 4: + case 5: + case 8: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'zehn'; + break; + + case 6: + $ret .= $this->_sep . 'sechzehn'; + break; + + case 7: + $ret .= $this->_sep . 'siebzehn'; + break; + } + break; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + if ($power == 3) + $ret .= $this->_sep . $lev[0]; + elseif ($d == 1 && ($t+$h) == 0) + $ret .= $this->_sep2 . $lev[0] . $this->_sep2; + else + $ret .= $this->_sep2 . $lev[1] . $this->_sep2; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.dk.php b/Numbers/Words/lang.dk.php new file mode 100644 index 0000000..80031e8 --- /dev/null +++ b/Numbers/Words/lang.dk.php @@ -0,0 +1,417 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.dk.php,v 1.2 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Danish language. +// + +/** + * Class for translating numbers into Danish. + * + * @author Jesper Veggerby + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Danish. + * + * @author Jesper Veggerby + * @package Numbers_Words + */ +class Numbers_Words_dk extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'dk'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Danish'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Dansk'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural). + * From: http://da.wikipedia.org/wiki/Navne_p%E5_store_tal + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('tusind','tusinde'), + 6 => array('million','millioner'), + 9 => array('milliard','milliarder'), + 12 => array('billion','billioner'), + 15 => array('billiard','billiarder'), + 18 => array('trillion','trillioner'), + 21 => array('trilliard','trilliarder'), + 24 => array('quadrillion','quadrillioner'), + 30 => array('quintillion','quintillioner'), + 36 => array('sextillion','sextillioner'), + 42 => array('septillion','septillioner'), + 48 => array('octillion','octillioner'), + 54 => array('nonillion','nonillioner'), + 60 => array('decillion','decillioner'), + 120 => array('vigintillion','vigintillioner'), + 600 => array('centillion','centillioner') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'nul', 'en', 'to', 'tre', 'fire', + 'fem', 'seks', 'syv', 'otte', 'ni' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://da.wikipedia.org/wiki/Valuta + * @access private + */ + var $_currency_names = array( + 'AUD' => array(array('australsk dollar', 'australske dollars'), array('cent')), + 'CAD' => array(array('canadisk dollar', 'canadisk dollars'), array('cent')), + 'CHF' => array(array('schweitzer franc'), array('rappen')), + 'CYP' => array(array('cypriotisk pund', 'cypriotiske pund'), array('cent')), + 'CZK' => array(array('tjekkisk koruna'), array('halerz')), + 'DKK' => array(array('krone', 'kroner'), array('øre')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pund'), array('pence')), + 'HKD' => array(array('Hong Kong dollar', 'Hong Kong dollars'), array('cent')), + 'JPY' => array(array('yen'), array('sen')), + 'NOK' => array(array('norsk krone', 'norske kroner'), array('øre')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'SEK' => array(array('svensk krone', 'svenske kroner'), array('øre')), + 'USD' => array(array('dollar', 'dollars'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'DKK'; // Danish krone + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Danish language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Jesper Veggerby + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + if ($h == 1) { + $ret .= $this->_sep . 'et' . $this->_sep . 'hundrede'; + } else { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundrede'; + } + + //if (($t + $d) > 0) + // $ret .= $this->_sep . 'og'; + } elseif ((isset($maxp)) && ($maxp > 3)) { + // add 'og' in the case where there are preceding thousands but not hundreds or tens, + // so fx. 80001 becomes 'firs tusinde og en' instead of 'firs tusinde en' + $ret .= $this->_sep . 'og'; + } + + + if ($t != 1 && $d > 0) { + $ret .= $this->_sep . (($d == 1 & $power == 3 && $t == 0 && $h == 0) ? "et" : $this->_digits[$d]) . ($t > 1 ? $this->_sep . "og" : ""); + } + + // ten, twenty etc. + switch ($t) { + case 9: + $ret .= $this->_sep . 'halvfems'; + break; + + case 8: + $ret .= $this->_sep . 'firs'; + break; + + case 7: + $ret .= $this->_sep . 'halvfjerds'; + break; + + case 6: + $ret .= $this->_sep . 'tres'; + break; + + case 5: + $ret .= $this->_sep . 'halvtreds'; + break; + + case 4: + $ret .= $this->_sep . 'fyrre'; + break; + + case 3: + $ret .= $this->_sep . 'tredive'; + break; + + case 2: + $ret .= $this->_sep . 'tyve'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'ti'; + break; + + case 1: + $ret .= $this->_sep . 'elleve'; + break; + + case 2: + $ret .= $this->_sep . 'tolv'; + break; + + case 3: + $ret .= $this->_sep . 'tretten'; + break; + + case 4: + $ret .= $this->_sep . 'fjorten'; + break; + + case 5: + $ret .= $this->_sep . 'femten'; + break; + + case 6: + $ret .= $this->_sep . 'seksten'; + break; + + case 7: + $ret .= $this->_sep . 'sytten'; + break; + + case 8: + $ret .= $this->_sep . 'atten'; + break; + + case 9: + $ret .= $this->_sep . 'nitten'; + break; + + } + break; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + if ($d == 1 && ($t+$h) == 0) { + $ret .= $this->_sep . $lev[0]; + } else { + $ret .= $this->_sep . $lev[1]; + } + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in danish language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Jesper Veggerby + * @since Numbers_Words 0.4 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + + if (($decimal != "") and ($decimal != 0)) { + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + + if (($fraction !== false) and ($fraction != 0)) { + $ret .= $this->_sep . "og"; + } + } + + if (($fraction !== false) and ($fraction != 0)) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + } + +?> diff --git a/Numbers/Words/lang.en_100.php b/Numbers/Words/lang.en_100.php new file mode 100644 index 0000000..65d24ed --- /dev/null +++ b/Numbers/Words/lang.en_100.php @@ -0,0 +1,307 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.en_100.php,v 1.2 2004/09/02 11:07:59 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Donald Knuth system, in English language. +// + +/** + * Class for translating numbers into Donald Knuth system, in English language. + * + * @author Piotr Klaban + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Donald Knuth system, in English language. + * + * @author Piotr Klaban + * @package Numbers_Words + */ +class Numbers_Words_en_100 extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'en_100'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'English (Donald Knuth system)'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'English (Donald Knuth system)'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names based on: + * http://home.earthlink.net/~mrob/pub/math/largenum.html + * Donald Knuth system (power of 2) + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 2 => array('hundred'), + 4 => array('myriad'), + 8 => array('myllion'), + 16 => array('byllion'), + 32 => array('tryllion'), + 64 => array('quadryllion'), + 128 => array('quintyllion'), + 256 => array('sextyllion'), + 512 => array('septyllion'), + 1024 => array('octyllion'), + 2048 => array('nonyllion'), + 4096 => array('decyllion'), + 8192 => array('undecyllion'), + 16384 => array('duodecyllion'), + 32768 => array('tredecyllion'), + 65536 => array('quattuordecyllion'), + 131072 => array('quindecyllion'), + 262144 => array('sexdecyllion'), + 524288 => array('septendecyllion'), + 1048576 => array('octodecyllion'), + 2097152 => array('novemdecyllion'), + 4194304 => array('vigintyllion') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + // }}} + // {{{ toWords() + /** + * Converts a number to its word representation + * in Donald Knuth system, in English language. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, ''); // $cursuffix); + // normally cursuffix is added at the end, but not here + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundred'; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 7: + case 6: + $ret .= $this->_sep . $this->_digits[$t] . 'ty'; + break; + + case 8: + $ret .= $this->_sep . 'eighty'; + break; + + case 5: + $ret .= $this->_sep . 'fifty'; + break; + + case 4: + $ret .= $this->_sep . 'forty'; + break; + + case 3: + $ret .= $this->_sep . 'thirty'; + break; + + case 2: + $ret .= $this->_sep . 'twenty'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'ten'; + break; + + case 1: + $ret .= $this->_sep . 'eleven'; + break; + + case 2: + $ret .= $this->_sep . 'twelve'; + break; + + case 3: + $ret .= $this->_sep . 'thirteen'; + break; + + case 4: + case 6: + case 7: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'teen'; + break; + + case 5: + $ret .= $this->_sep . 'fifteen'; + break; + + case 8: + $ret .= $this->_sep . 'eighteen'; + break; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + if ($t > 1) { + $ret .= '-' . $this->_digits[$d]; + } else { + $ret .= $this->_sep . $this->_digits[$d]; + } + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.en_GB.php b/Numbers/Words/lang.en_GB.php new file mode 100644 index 0000000..cc227c8 --- /dev/null +++ b/Numbers/Words/lang.en_GB.php @@ -0,0 +1,424 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.en_GB.php,v 1.4 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in British English language. +// + +/** + * Class for translating numbers into British English. + * + * @author Piotr Klaban + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into British English. + * + * @author Piotr Klaban + * @package Numbers_Words + */ +class Numbers_Words_en_GB extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'en_GB'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'British English'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'British English'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://www.users.dircon.co.uk/~shaunf/shaun/numbers/millions.htm + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('thousand'), + 6 => array('million'), + 12 => array('billion'), + 18 => array('trillion'), + 24 => array('quadrillion'), + 30 => array('quintillion'), + 36 => array('sextillion'), + 42 => array('septillion'), + 48 => array('octillion'), + 54 => array('nonillion'), + 60 => array('decillion'), + 66 => array('undecillion'), + 72 => array('duodecillion'), + 78 => array('tredecillion'), + 84 => array('quattuordecillion'), + 90 => array('quindecillion'), + 96 => array('sexdecillion'), + 102 => array('septendecillion'), + 108 => array('octodecillion'), + 114 => array('novemdecillion'), + 120 => array('vigintillion'), + 192 => array('duotrigintillion'), + 600 => array('centillion') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek'), array('qindarka')), + 'AUD' => array(array('Australian dollar'), array('cent')), + 'BAM' => array(array('convertible marka'), array('fenig')), + 'BGN' => array(array('lev'), array('stotinka')), + 'BRL' => array(array('real'), array('centavos')), + 'BYR' => array(array('Belarussian rouble'), array('kopiejka')), + 'CAD' => array(array('Canadian dollar'), array('cent')), + 'CHF' => array(array('Swiss franc'), array('rapp')), + 'CYP' => array(array('Cypriot pound'), array('cent')), + 'CZK' => array(array('Czech koruna'), array('halerz')), + 'DKK' => array(array('Danish krone'), array('ore')), + 'EEK' => array(array('kroon'), array('senti')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pound', 'pounds'), array('pence', 'pence')), + 'HKD' => array(array('Hong Kong dollar'), array('cent')), + 'HRK' => array(array('Croatian kuna'), array('lipa')), + 'HUF' => array(array('forint'), array('filler')), + 'ILS' => array(array('new sheqel','new sheqels'), array('agora','agorot')), + 'ISK' => array(array('Icelandic króna'), array('aurar')), + 'JPY' => array(array('yen'), array('sen')), + 'LTL' => array(array('litas'), array('cent')), + 'LVL' => array(array('lat'), array('sentim')), + 'MKD' => array(array('Macedonian dinar'), array('deni')), + 'MTL' => array(array('Maltese lira'), array('centym')), + 'NOK' => array(array('Norwegian krone'), array('oere')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'ROL' => array(array('Romanian leu'), array('bani')), + 'RUB' => array(array('Russian Federation rouble'), array('kopiejka')), + 'SEK' => array(array('Swedish krona'), array('oere')), + 'SIT' => array(array('Tolar'), array('stotinia')), + 'SKK' => array(array('Slovak koruna'), array()), + 'TRL' => array(array('lira'), array('kuruþ')), + 'UAH' => array(array('hryvna'), array('cent')), + 'USD' => array(array('dollar'), array('cent')), + 'YUM' => array(array('dinars'), array('para')), + 'ZAR' => array(array('rand'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'GBP'; // English pound + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in British English language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundred'; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 7: + case 6: + $ret .= $this->_sep . $this->_digits[$t] . 'ty'; + break; + + case 8: + $ret .= $this->_sep . 'eighty'; + break; + + case 5: + $ret .= $this->_sep . 'fifty'; + break; + + case 4: + $ret .= $this->_sep . 'forty'; + break; + + case 3: + $ret .= $this->_sep . 'thirty'; + break; + + case 2: + $ret .= $this->_sep . 'twenty'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'ten'; + break; + + case 1: + $ret .= $this->_sep . 'eleven'; + break; + + case 2: + $ret .= $this->_sep . 'twelve'; + break; + + case 3: + $ret .= $this->_sep . 'thirteen'; + break; + + case 4: + case 6: + case 7: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'teen'; + break; + + case 5: + $ret .= $this->_sep . 'fifteen'; + break; + + case 8: + $ret .= $this->_sep . 'eighteen'; + break; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + if ($t > 1) { + $ret .= '-' . $this->_digits[$d]; + } else { + $ret .= $this->_sep . $this->_digits[$d]; + } + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in English language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Piotr Klaban + * @since Numbers_Words 0.13.1 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + $ret .= $this->_sep . $curr_names[0][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + + +} + +?> diff --git a/Numbers/Words/lang.en_US.php b/Numbers/Words/lang.en_US.php new file mode 100644 index 0000000..bcbdd6b --- /dev/null +++ b/Numbers/Words/lang.en_US.php @@ -0,0 +1,515 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.en_US.php,v 1.5 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in American English language. +// + +/** + * Class for translating numbers into American English. + * + * @author Piotr Klaban + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into American English. + * + * @author Piotr Klaban + * @package Numbers_Words + */ +class Numbers_Words_en_US extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'en_US'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'American English'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'American English'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://home.earthlink.net/~mrob/pub/math/largenum.html + * http://mathforum.org/dr.math/faq/faq.large.numbers.html + * http://www.mazes.com/AmericanNumberingSystem.html + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('thousand'), + 6 => array('million'), + 9 => array('billion'), + 12 => array('trillion'), + 15 => array('quadrillion'), + 18 => array('quintillion'), + 21 => array('sextillion'), + 24 => array('septillion'), + 27 => array('octillion'), + 30 => array('nonillion'), + 33 => array('decillion'), + 36 => array('undecillion'), + 39 => array('duodecillion'), + 42 => array('tredecillion'), + 45 => array('quattuordecillion'), + 48 => array('quindecillion'), + 51 => array('sexdecillion'), + 54 => array('septendecillion'), + 57 => array('octodecillion'), + 60 => array('novemdecillion'), + 63 => array('vigintillion'), + 66 => array('unvigintillion'), + 69 => array('duovigintillion'), + 72 => array('trevigintillion'), + 75 => array('quattuorvigintillion'), + 78 => array('quinvigintillion'), + 81 => array('sexvigintillion'), + 84 => array('septenvigintillion'), + 87 => array('octovigintillion'), + 90 => array('novemvigintillion'), + 93 => array('trigintillion'), + 96 => array('untrigintillion'), + 99 => array('duotrigintillion'), + // 100 => array('googol') - not latin name + // 10^googol = 1 googolplex + 102 => array('trestrigintillion'), + 105 => array('quattuortrigintillion'), + 108 => array('quintrigintillion'), + 111 => array('sextrigintillion'), + 114 => array('septentrigintillion'), + 117 => array('octotrigintillion'), + 120 => array('novemtrigintillion'), + 123 => array('quadragintillion'), + 126 => array('unquadragintillion'), + 129 => array('duoquadragintillion'), + 132 => array('trequadragintillion'), + 135 => array('quattuorquadragintillion'), + 138 => array('quinquadragintillion'), + 141 => array('sexquadragintillion'), + 144 => array('septenquadragintillion'), + 147 => array('octoquadragintillion'), + 150 => array('novemquadragintillion'), + 153 => array('quinquagintillion'), + 156 => array('unquinquagintillion'), + 159 => array('duoquinquagintillion'), + 162 => array('trequinquagintillion'), + 165 => array('quattuorquinquagintillion'), + 168 => array('quinquinquagintillion'), + 171 => array('sexquinquagintillion'), + 174 => array('septenquinquagintillion'), + 177 => array('octoquinquagintillion'), + 180 => array('novemquinquagintillion'), + 183 => array('sexagintillion'), + 186 => array('unsexagintillion'), + 189 => array('duosexagintillion'), + 192 => array('tresexagintillion'), + 195 => array('quattuorsexagintillion'), + 198 => array('quinsexagintillion'), + 201 => array('sexsexagintillion'), + 204 => array('septensexagintillion'), + 207 => array('octosexagintillion'), + 210 => array('novemsexagintillion'), + 213 => array('septuagintillion'), + 216 => array('unseptuagintillion'), + 219 => array('duoseptuagintillion'), + 222 => array('treseptuagintillion'), + 225 => array('quattuorseptuagintillion'), + 228 => array('quinseptuagintillion'), + 231 => array('sexseptuagintillion'), + 234 => array('septenseptuagintillion'), + 237 => array('octoseptuagintillion'), + 240 => array('novemseptuagintillion'), + 243 => array('octogintillion'), + 246 => array('unoctogintillion'), + 249 => array('duooctogintillion'), + 252 => array('treoctogintillion'), + 255 => array('quattuoroctogintillion'), + 258 => array('quinoctogintillion'), + 261 => array('sexoctogintillion'), + 264 => array('septoctogintillion'), + 267 => array('octooctogintillion'), + 270 => array('novemoctogintillion'), + 273 => array('nonagintillion'), + 276 => array('unnonagintillion'), + 279 => array('duononagintillion'), + 282 => array('trenonagintillion'), + 285 => array('quattuornonagintillion'), + 288 => array('quinnonagintillion'), + 291 => array('sexnonagintillion'), + 294 => array('septennonagintillion'), + 297 => array('octononagintillion'), + 300 => array('novemnonagintillion'), + 303 => array('centillion'), + 309 => array('duocentillion'), + 312 => array('trecentillion'), + 366 => array('primo-vigesimo-centillion'), + 402 => array('trestrigintacentillion'), + 603 => array('ducentillion'), + 624 => array('septenducentillion'), + // bug on a earthlink page: 903 => array('trecentillion'), + 2421 => array('sexoctingentillion'), + 3003 => array('millillion'), + 3000003 => array('milli-millillion') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek'), array('qindarka')), + 'AUD' => array(array('Australian dollar'), array('cent')), + 'BAM' => array(array('convertible marka'), array('fenig')), + 'BGN' => array(array('lev'), array('stotinka')), + 'BRL' => array(array('real'), array('centavos')), + 'BYR' => array(array('Belarussian rouble'), array('kopiejka')), + 'CAD' => array(array('Canadian dollar'), array('cent')), + 'CHF' => array(array('Swiss franc'), array('rapp')), + 'CYP' => array(array('Cypriot pound'), array('cent')), + 'CZK' => array(array('Czech koruna'), array('halerz')), + 'DKK' => array(array('Danish krone'), array('ore')), + 'EEK' => array(array('kroon'), array('senti')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pound', 'pounds'), array('pence', 'pence')), + 'HKD' => array(array('Hong Kong dollar'), array('cent')), + 'HRK' => array(array('Croatian kuna'), array('lipa')), + 'HUF' => array(array('forint'), array('filler')), + 'ILS' => array(array('new sheqel','new sheqels'), array('agora','agorot')), + 'ISK' => array(array('Icelandic króna'), array('aurar')), + 'JPY' => array(array('yen'), array('sen')), + 'LTL' => array(array('litas'), array('cent')), + 'LVL' => array(array('lat'), array('sentim')), + 'MKD' => array(array('Macedonian dinar'), array('deni')), + 'MTL' => array(array('Maltese lira'), array('centym')), + 'NOK' => array(array('Norwegian krone'), array('oere')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'ROL' => array(array('Romanian leu'), array('bani')), + 'RUB' => array(array('Russian Federation rouble'), array('kopiejka')), + 'SEK' => array(array('Swedish krona'), array('oere')), + 'SIT' => array(array('Tolar'), array('stotinia')), + 'SKK' => array(array('Slovak koruna'), array()), + 'TRL' => array(array('lira'), array('kuruþ')), + 'UAH' => array(array('hryvna'), array('cent')), + 'USD' => array(array('dollar'), array('cent')), + 'YUM' => array(array('dinars'), array('para')), + 'ZAR' => array(array('rand'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'USD'; // American dollar + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in American English language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundred'; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 7: + case 6: + $ret .= $this->_sep . $this->_digits[$t] . 'ty'; + break; + + case 8: + $ret .= $this->_sep . 'eighty'; + break; + + case 5: + $ret .= $this->_sep . 'fifty'; + break; + + case 4: + $ret .= $this->_sep . 'forty'; + break; + + case 3: + $ret .= $this->_sep . 'thirty'; + break; + + case 2: + $ret .= $this->_sep . 'twenty'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'ten'; + break; + + case 1: + $ret .= $this->_sep . 'eleven'; + break; + + case 2: + $ret .= $this->_sep . 'twelve'; + break; + + case 3: + $ret .= $this->_sep . 'thirteen'; + break; + + case 4: + case 6: + case 7: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'teen'; + break; + + case 5: + $ret .= $this->_sep . 'fifteen'; + break; + + case 8: + $ret .= $this->_sep . 'eighteen'; + break; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + if ($t > 1) { + $ret .= '-' . $this->_digits[$d]; + } else { + $ret .= $this->_sep . $this->_digits[$d]; + } + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in English language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Piotr Klaban + * @since Numbers_Words 0.4 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + $ret .= $this->_sep . $curr_names[0][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + +} + +?> diff --git a/Numbers/Words/lang.es.php b/Numbers/Words/lang.es.php new file mode 100644 index 0000000..267937c --- /dev/null +++ b/Numbers/Words/lang.es.php @@ -0,0 +1,343 @@ + array('',''), + 3 => array('mil','mil'), + 6 => array('millón','millones'), + 12 => array('billón','billones'), + 18 => array('trilón','trillones'), + 24 => array('cuatrillón','cuatrillones'), + 30 => array('quintillón','quintillones'), + 36 => array('sextillón','sextillones'), + 42 => array('septillón','septillones'), + 48 => array('octallón','octallones'), + 54 => array('nonallón','nonallones'), + 60 => array('decallón','decallones'), + ); + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'cero', 'uno', 'dos', 'tres', 'cuatro', + 'cinco', 'seis', 'siete', 'ocho', 'nueve' + ); + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + // }}} + // {{{ toWords() + /** + * Converts a number to its word representation + * in Spanish (Castellano). + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that should be converted to a words representation + * @param integer $power The power of ten for the rest of the number to the right. + * For example toWords(12,3) should give "doce mil". + * Optional, defaults to 0. + * @return string The corresponding word representation + * + * @access private + * @author Xavier Noguer + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0) + { + // The return string; + $ret = ''; + + // add a the word for the minus sign if necessary + if (substr($num, 0, 1) == '-') + { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + + // strip excessive zero signs + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 6) + { + $current_power = 6; + // check for highest power + if (isset($this->_exponent[$power])) + { + // convert the number above the first 6 digits + // with it's corresponding $power. + $snum = substr($num, 0, -6); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $ret .= $this->toWords($snum, $power + 6); + } + } + $num = substr($num, -6); + if ($num == 0) { + return $ret; + } + } + elseif ($num == 0 || $num == '') { + return(' '.$this->_digits[0]); + $current_power = strlen($num); + } + else { + $current_power = strlen($num); + } + + // See if we need "thousands" + $thousands = floor($num / 1000); + if ($thousands == 1) { + $ret .= $this->_sep . 'mil'; + } + elseif ($thousands > 1) { + $ret .= $this->toWords($thousands, 3); + } + + // values for digits, tens and hundreds + $h = floor(($num / 100) % 10); + $t = floor(($num / 10) % 10); + $d = floor($num % 10); + + // cientos: doscientos, trescientos, etc... + switch ($h) + { + case 1: + if (($d == 0) and ($t == 0)) { // is it's '100' use 'cien' + $ret .= $this->_sep . 'cien'; + } + else { + $ret .= $this->_sep . 'ciento'; + } + break; + case 2: + case 3: + case 4: + case 6: + case 8: + $ret .= $this->_sep . $this->_digits[$h] . 'cientos'; + break; + case 5: + $ret .= $this->_sep . 'quinientos'; + break; + case 7: + $ret .= $this->_sep . 'setecientos'; + break; + case 9: + $ret .= $this->_sep . 'novecientos'; + break; + } + + // decenas: veinte, treinta, etc... + switch ($t) + { + case 9: + $ret .= $this->_sep . 'noventa'; + break; + + case 8: + $ret .= $this->_sep . 'ochenta'; + break; + + case 7: + $ret .= $this->_sep . 'setenta'; + break; + + case 6: + $ret .= $this->_sep . 'sesenta'; + break; + + case 5: + $ret .= $this->_sep . 'cincuenta'; + break; + + case 4: + $ret .= $this->_sep . 'cuarenta'; + break; + + case 3: + $ret .= $this->_sep . 'treinta'; + break; + + case 2: + if ($d == 0) { + $ret .= $this->_sep . 'veinte'; + } + else { + if (($power > 0) and ($d == 1)) { + $ret .= $this->_sep . 'veintiún'; + } + else { + $ret .= $this->_sep . 'veinti' . $this->_digits[$d]; + } + } + break; + + case 1: + switch ($d) + { + case 0: + $ret .= $this->_sep . 'diez'; + break; + + case 1: + $ret .= $this->_sep . 'once'; + break; + + case 2: + $ret .= $this->_sep . 'doce'; + break; + + case 3: + $ret .= $this->_sep . 'trece'; + break; + + case 4: + $ret .= $this->_sep . 'catorce'; + break; + + case 5: + $ret .= $this->_sep . 'quince'; + break; + + case 6: + case 7: + case 9: + case 8: + $ret .= $this->_sep . 'dieci' . $this->_digits[$d]; + break; + } + break; + } + + // add digits only if it is a multiple of 10 and not 1x or 2x + if (($t != 1) and ($t != 2) and ($d > 0)) + { + if($t != 0) // don't add 'y' for numbers below 10 + { + // use 'un' instead of 'uno' when there is a suffix ('mil', 'millones', etc...) + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.' y un'; + } + else { + $ret .= $this->_sep.'y '.$this->_digits[$d]; + } + } + else { + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.'un'; + } + else { + $ret .= $this->_sep.$this->_digits[$d]; + } + } + } + + if ($power > 0) + { + if (isset($this->_exponent[$power])) { + $lev = $this->_exponent[$power]; + } + + if (!isset($lev) || !is_array($lev)) { + return null; + } + + // if it's only one use the singular suffix + if (($d == 1) and ($t == 0) and ($h == 0)) { + $suffix = $lev[0]; + } + else { + $suffix = $lev[1]; + } + if ($num != 0) { + $ret .= $this->_sep . $suffix; + } + } + + return $ret; + } + // }}} +} +?> diff --git a/Numbers/Words/lang.es_AR.php b/Numbers/Words/lang.es_AR.php new file mode 100644 index 0000000..d3ff272 --- /dev/null +++ b/Numbers/Words/lang.es_AR.php @@ -0,0 +1,474 @@ + | +// | Based On: lang_es.php - Xavier Noguer | +// +----------------------------------------------------------------------+ +// $Id: lang.es_AR.php,v 1.3 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Argentinian Spanish +// +// + +/** + * Class for translating numbers into Argentinian Spanish. + * + * @author Martin Marrese + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Argentinian Spanish. + * It supports up to decallones (10^6). + * It doesn't support spanish tonic accents (acentos). + * + * @author Martin Marrese + * @package Numbers_Words + */ +class Numbers_Words_es_AR extends Numbers_Words +{ + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'es_AR'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Spanish'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Español'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'menos'; + + /** + * The sufixes for exponents (singular and plural) + * @var array + * @access private + */ + var $_exponent = array( + 0 => array('',''), + 3 => array('mil','mil'), + 6 => array('millón','millones'), + 12 => array('billón','billones'), + 18 => array('trilón','trillones'), + 24 => array('cuatrillón','cuatrillones'), + 30 => array('quintillón','quintillones'), + 36 => array('sextillón','sextillones'), + 42 => array('septillón','septillones'), + 48 => array('octallón','octallones'), + 54 => array('nonallón','nonallones'), + 60 => array('decallón','decallones'), + ); + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'cero', 'uno', 'dos', 'tres', 'cuatro', + 'cinco', 'seis', 'siete', 'ocho', 'nueve' + ); + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek'), array('qindarka')), + 'AUD' => array(array('Australian dollar'), array('cent')), + 'ARS' => array(array('Peso'), array ('centavo')), + 'BAM' => array(array('convertible marka'), array('fenig')), + 'BGN' => array(array('lev'), array('stotinka')), + 'BRL' => array(array('real'), array('centavos')), + 'BYR' => array(array('Belarussian rouble'), array('kopiejka')), + 'CAD' => array(array('Canadian dollar'), array('cent')), + 'CHF' => array(array('Swiss franc'), array('rapp')), + 'CYP' => array(array('Cypriot pound'), array('cent')), + 'CZK' => array(array('Czech koruna'), array('halerz')), + 'DKK' => array(array('Danish krone'), array('ore')), + 'EEK' => array(array('kroon'), array('senti')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pound', 'pounds'), array('pence')), + 'HKD' => array(array('Hong Kong dollar'), array('cent')), + 'HRK' => array(array('Croatian kuna'), array('lipa')), + 'HUF' => array(array('forint'), array('filler')), + 'ILS' => array(array('new sheqel','new sheqels'), array('agora','agorot')), + 'ISK' => array(array('Icelandic króna'), array('aurar')), + 'JPY' => array(array('yen'), array('sen')), + 'LTL' => array(array('litas'), array('cent')), + 'LVL' => array(array('lat'), array('sentim')), + 'MKD' => array(array('Macedonian dinar'), array('deni')), + 'MTL' => array(array('Maltese lira'), array('centym')), + 'NOK' => array(array('Norwegian krone'), array('oere')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'ROL' => array(array('Romanian leu'), array('bani')), + 'RUB' => array(array('Russian Federation rouble'), array('kopiejka')), + 'SEK' => array(array('Swedish krona'), array('oere')), + 'SIT' => array(array('Tolar'), array('stotinia')), + 'SKK' => array(array('Slovak koruna'), array()), + 'TRL' => array(array('lira'), array('kuruþ')), + 'UAH' => array(array('hryvna'), array('cent')), + 'USD' => array(array('dollar'), array('cent')), + 'YUM' => array(array('dinars'), array('para')), + 'ZAR' => array(array('rand'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'ARS'; // Argentinian Peso + + // }}} + // {{{ toWords() + /** + * Converts a number to its word representation + * in Argentinian Spanish. + * + * @param float $num An float between -infinity and infinity inclusive :) + * that should be converted to a words representation + * @param integer $power The power of ten for the rest of the number to the right. + * For example toWords(12,3) should give "doce mil". + * Optional, defaults to 0. + * @return string The corresponding word representation + * + * @access private + * @author Martin Marrese + */ + function toWords($num, $power = 0) + { + // The return string; + $ret = ''; + + // add a the word for the minus sign if necessary + if (substr($num, 0, 1) == '-') + { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + + // strip excessive zero signs + $num = preg_replace('/^0+/','',$num); + + $num_tmp = split ('\.', $num); + + $num = $num_tmp[0]; + $dec = (@$num_tmp[1]) ? $num_tmp[1] : ''; + + if (strlen($num) > 6) + { + $current_power = 6; + // check for highest power + if (isset($this->_exponent[$power])) + { + // convert the number above the first 6 digits + // with it's corresponding $power. + $snum = substr($num, 0, -6); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $ret .= $this->toWords($snum, $power + 6); + } + } + $num = substr($num, -6); + if ($num == 0) { + return $ret; + } + } + elseif ($num == 0 || $num == '') { + return(' '.$this->_digits[0]); + $current_power = strlen($num); + } + else { + $current_power = strlen($num); + } + + // See if we need "thousands" + $thousands = floor($num / 1000); + if ($thousands == 1) { + $ret .= $this->_sep . 'mil'; + } + elseif ($thousands > 1) { + $ret .= $this->toWords($thousands, 3); + } + + // values for digits, tens and hundreds + $h = floor(($num / 100) % 10); + $t = floor(($num / 10) % 10); + $d = floor($num % 10); + + // cientos: doscientos, trescientos, etc... + switch ($h) + { + case 1: + if (($d == 0) and ($t == 0)) { // is it's '100' use 'cien' + $ret .= $this->_sep . 'cien'; + } + else { + $ret .= $this->_sep . 'ciento'; + } + break; + case 2: + case 3: + case 4: + case 6: + case 8: + $ret .= $this->_sep . $this->_digits[$h] . 'cientos'; + break; + case 5: + $ret .= $this->_sep . 'quinientos'; + break; + case 7: + $ret .= $this->_sep . 'setecientos'; + break; + case 9: + $ret .= $this->_sep . 'novecientos'; + break; + } + + // decenas: veinte, treinta, etc... + switch ($t) + { + case 9: + $ret .= $this->_sep . 'noventa'; + break; + + case 8: + $ret .= $this->_sep . 'ochenta'; + break; + + case 7: + $ret .= $this->_sep . 'setenta'; + break; + + case 6: + $ret .= $this->_sep . 'sesenta'; + break; + + case 5: + $ret .= $this->_sep . 'cincuenta'; + break; + + case 4: + $ret .= $this->_sep . 'cuarenta'; + break; + + case 3: + $ret .= $this->_sep . 'treinta'; + break; + + case 2: + if ($d == 0) { + $ret .= $this->_sep . 'veinte'; + } + else { + if (($power > 0) and ($d == 1)) { + $ret .= $this->_sep . 'veintiún'; + } + else { + $ret .= $this->_sep . 'veinti' . $this->_digits[$d]; + } + } + break; + + case 1: + switch ($d) + { + case 0: + $ret .= $this->_sep . 'diez'; + break; + + case 1: + $ret .= $this->_sep . 'once'; + break; + + case 2: + $ret .= $this->_sep . 'doce'; + break; + + case 3: + $ret .= $this->_sep . 'trece'; + break; + + case 4: + $ret .= $this->_sep . 'catorce'; + break; + + case 5: + $ret .= $this->_sep . 'quince'; + break; + + case 6: + case 7: + case 9: + case 8: + $ret .= $this->_sep . 'dieci' . $this->_digits[$d]; + break; + } + break; + } + + // add digits only if it is a multiple of 10 and not 1x or 2x + if (($t != 1) and ($t != 2) and ($d > 0)) + { + if($t != 0) // don't add 'y' for numbers below 10 + { + // use 'un' instead of 'uno' when there is a suffix ('mil', 'millones', etc...) + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.' y un'; + } + else { + $ret .= $this->_sep.'y '.$this->_digits[$d]; + } + } + else { + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.'un'; + } + else { + $ret .= $this->_sep.$this->_digits[$d]; + } + } + } + + if ($power > 0) + { + if (isset($this->_exponent[$power])) { + $lev = $this->_exponent[$power]; + } + + if (!isset($lev) || !is_array($lev)) { + return null; + } + + // if it's only one use the singular suffix + if (($d == 1) and ($t == 0) and ($h == 0)) { + $suffix = $lev[0]; + } + else { + $suffix = $lev[1]; + } + if ($num != 0) { + $ret .= $this->_sep . $suffix; + } + } + + if ($dec) { + $dec = $this->toWords(trim($dec)); + $ret.= ' con ' . trim ($dec); + } + + return $ret; + } + // }}} + + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in Agentinian Spanish language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Martin Marrese + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret = $curr_names[0][$lev]; + } else { + $ret = $curr_names[0][0] . 's'; + } + } else { + $ret = $curr_names[0][0]; + } + $ret .= $this->_sep . trim($this->toWords($decimal)); + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep .'con'. $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep .'con'. $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + + + +} +?> diff --git a/Numbers/Words/lang.et.php b/Numbers/Words/lang.et.php new file mode 100644 index 0000000..aecb68d --- /dev/null +++ b/Numbers/Words/lang.et.php @@ -0,0 +1,349 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.et.php,v 1.1 2006/06/12 13:50:07 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Estonian language. +// + +/** + * Class for translating numbers into Estonian. + * + * @author Erkki Saarniit + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Estonian. + * + * @author Erkki Saarniit + * @package Numbers_Words + */ +class Numbers_Words_et extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'et'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Estonian'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'eesti keel'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'miinus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://home.earthlink.net/~mrob/pub/math/largenum.html + * http://mathforum.org/dr.math/faq/faq.large.numbers.html + * http://www.mazes.com/AmericanNumberingSystem.html + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('tuhat'), + 6 => array('miljon'), + 9 => array('miljard'), + 12 => array('triljon'), + 15 => array('kvadriljon'), + 18 => array('kvintiljon'), + 21 => array('sekstiljon'), + 24 => array('septiljon'), + 27 => array('oktiljon'), + 30 => array('noniljon'), + 33 => array('dekiljon'), + 36 => array('undekiljon'), + 39 => array('duodekiljon'), + 42 => array('tredekiljon'), + 45 => array('kvattuordekiljon'), + 48 => array('kvindekiljon'), + 51 => array('seksdekiljon'), + 54 => array('septendekiljon'), + 57 => array('oktodekiljon'), + 60 => array('novemdekiljon'), + 63 => array('vigintiljon'), + 66 => array('unvigintiljon'), + 69 => array('duovigintiljon'), + 72 => array('trevigintiljon'), + 75 => array('kvattuorvigintiljon'), + 78 => array('kvinvigintiljon'), + 81 => array('seksvigintiljon'), + 84 => array('septenvigintiljon'), + 87 => array('oktovigintiljon'), + 90 => array('novemvigintiljon'), + 93 => array('trigintiljon'), + 96 => array('untrigintiljon'), + 99 => array('duotrigintiljon'), + 102 => array('trestrigintiljon'), + 105 => array('kvattuortrigintiljon'), + 108 => array('kvintrigintiljon'), + 111 => array('sekstrigintiljon'), + 114 => array('septentrigintiljon'), + 117 => array('oktotrigintiljon'), + 120 => array('novemtrigintiljon'), + 123 => array('kvadragintiljon'), + 126 => array('unkvadragintiljon'), + 129 => array('duokvadragintiljon'), + 132 => array('trekvadragintiljon'), + 135 => array('kvattuorkvadragintiljon'), + 138 => array('kvinkvadragintiljon'), + 141 => array('sekskvadragintiljon'), + 144 => array('septenkvadragintiljon'), + 147 => array('oktokvadragintiljon'), + 150 => array('novemkvadragintiljon'), + 153 => array('kvinkvagintiljon'), + 156 => array('unkvinkvagintiljon'), + 159 => array('duokvinkvagintiljon'), + 162 => array('trekvinkvagintiljon'), + 165 => array('kvattuorkvinkvagintiljon'), + 168 => array('kvinkvinkvagintiljon'), + 171 => array('sekskvinkvagintiljon'), + 174 => array('septenkvinkvagintiljon'), + 177 => array('oktokvinkvagintiljon'), + 180 => array('novemkvinkvagintiljon'), + 183 => array('seksagintiljon'), + 186 => array('unseksagintiljon'), + 189 => array('duoseksagintiljon'), + 192 => array('treseksagintiljon'), + 195 => array('kvattuorseksagintiljon'), + 198 => array('kvinseksagintiljon'), + 201 => array('seksseksagintiljon'), + 204 => array('septenseksagintiljon'), + 207 => array('oktoseksagintiljon'), + 210 => array('novemseksagintiljon'), + 213 => array('septuagintiljon'), + 216 => array('unseptuagintiljon'), + 219 => array('duoseptuagintiljon'), + 222 => array('treseptuagintiljon'), + 225 => array('kvattuorseptuagintiljon'), + 228 => array('kvinseptuagintiljon'), + 231 => array('seksseptuagintiljon'), + 234 => array('septenseptuagintiljon'), + 237 => array('oktoseptuagintiljon'), + 240 => array('novemseptuagintiljon'), + 243 => array('oktogintiljon'), + 246 => array('unoktogintiljon'), + 249 => array('duooktogintiljon'), + 252 => array('treoktogintiljon'), + 255 => array('kvattuoroktogintiljon'), + 258 => array('kvinoktogintiljon'), + 261 => array('seksoktogintiljon'), + 264 => array('septoktogintiljon'), + 267 => array('oktooktogintiljon'), + 270 => array('novemoktogintiljon'), + 273 => array('nonagintiljon'), + 276 => array('unnonagintiljon'), + 279 => array('duononagintiljon'), + 282 => array('trenonagintiljon'), + 285 => array('kvattuornonagintiljon'), + 288 => array('kvinnonagintiljon'), + 291 => array('seksnonagintiljon'), + 294 => array('septennonagintiljon'), + 297 => array('oktononagintiljon'), + 300 => array('novemnonagintiljon'), + 303 => array('kentiljon'), + 309 => array('duokentiljon'), + 312 => array('trekentiljon'), + 366 => array('primo-vigesimo-kentiljon'), + 402 => array('trestrigintakentiljon'), + 603 => array('dukentiljon'), + 624 => array('septendukentiljon'), + 2421 => array('seksoktingentiljon'), + 3003 => array('milliljon'), + 3000003 => array('milli-milliljon') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'null', 'üks', 'kaks', 'kolm', 'neli', + 'viis', 'kuus', 'seitse', 'kaheksa', 'üheksa' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Estonian language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + if (isset($this->_exponent[$p])) { + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . 'sada'; + + } + + switch ($t) { + case 9: + case 8: + case 7: + case 6: + case 5: + case 4: + case 3: + case 2: + $ret .= $this->_sep . $this->_digits[$t] . 'kümmend'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'kümme'; + break; + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'teist'; + break; + } + break; + } + if ($t != 1 && $d > 0) { + if ($t > 1) { + $ret .= ' ' . $this->_digits[$d]; + } else { + $ret .= $this->_sep . $this->_digits[$d]; + } + } + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + $ret .= $this->_sep . $lev[0].($num != 1 && $power!= 3 ? 'it' : ''); + } + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.fr.php b/Numbers/Words/lang.fr.php new file mode 100644 index 0000000..d044022 --- /dev/null +++ b/Numbers/Words/lang.fr.php @@ -0,0 +1,439 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.fr.php,v 1.4 2004/10/22 18:22:52 kouber Exp $ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into French. + * + * @author Kouber Saparev + * @package Numbers_Words + */ +class Numbers_Words_fr extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name. + * @var string + * @access public + */ + var $locale = 'fr'; + + /** + * Language name in English. + * @var string + * @access public + */ + var $lang = 'French'; + + /** + * Native language name. + * @var string + * @access public + */ + var $lang_native = 'Français'; + + /** + * The words for some numbers. + * @var string + * @access private + */ + var $_misc_numbers = array( + 10=>'dix', // 10 + 'onze', // 11 + 'douze', // 12 + 'treize', // 13 + 'quatorze', // 14 + 'quinze', // 15 + 'seize', // 16 + 20=>'vingt', // 20 + 30=>'trente', // 30 + 40=>'quarante', // 40 + 50=>'cinquante',// 50 + 60=>'soixante', // 60 + 100=>'cent' // 100 + ); + + + /** + * The words for digits (except zero). + * @var string + * @access private + */ + var $_digits = array(1=>"un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf"); + + /** + * The word for zero. + * @var string + * @access private + */ + var $_zero = 'zéro'; + + /** + * The word for infinity. + * @var string + * @access private + */ + var $_infinity = 'infini'; + + /** + * The word for the "and" language construct. + * @var string + * @access private + */ + var $_and = 'et'; + + /** + * The word separator. + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The dash liaison. + * @var string + * @access private + */ + var $_dash = '-'; + + /** + * The word for the minus sign. + * @var string + * @access private + */ + var $_minus = 'moins'; // minus sign + + /** + * The plural suffix (except for hundred). + * @var string + * @access private + */ + var $_plural = 's'; // plural suffix + + /** + * The suffixes for exponents (singular). + * @var array + * @access private + */ + var $_exponent = array( + 0 => '', + 3 => 'mille', + 6 => 'million', + 9 => 'milliard', + 12 => 'trillion', + 15 => 'quadrillion', + 18 => 'quintillion', + 21 => 'sextillion', + 24 => 'septillion', + 27 => 'octillion', + 30 => 'nonillion', + 33 => 'decillion', + 36 => 'undecillion', + 39 => 'duodecillion', + 42 => 'tredecillion', + 45 => 'quattuordecillion', + 48 => 'quindecillion', + 51 => 'sexdecillion', + 54 => 'septendecillion', + 57 => 'octodecillion', + 60 => 'novemdecillion', + 63 => 'vigintillion', + 66 => 'unvigintillion', + 69 => 'duovigintillion', + 72 => 'trevigintillion', + 75 => 'quattuorvigintillion', + 78 => 'quinvigintillion', + 81 => 'sexvigintillion', + 84 => 'septenvigintillion', + 87 => 'octovigintillion', + 90 => 'novemvigintillion', + 93 => 'trigintillion', + 96 => 'untrigintillion', + 99 => 'duotrigintillion', + 102 => 'trestrigintillion', + 105 => 'quattuortrigintillion', + 108 => 'quintrigintillion', + 111 => 'sextrigintillion', + 114 => 'septentrigintillion', + 117 => 'octotrigintillion', + 120 => 'novemtrigintillion', + 123 => 'quadragintillion', + 126 => 'unquadragintillion', + 129 => 'duoquadragintillion', + 132 => 'trequadragintillion', + 135 => 'quattuorquadragintillion', + 138 => 'quinquadragintillion', + 141 => 'sexquadragintillion', + 144 => 'septenquadragintillion', + 147 => 'octoquadragintillion', + 150 => 'novemquadragintillion', + 153 => 'quinquagintillion', + 156 => 'unquinquagintillion', + 159 => 'duoquinquagintillion', + 162 => 'trequinquagintillion', + 165 => 'quattuorquinquagintillion', + 168 => 'quinquinquagintillion', + 171 => 'sexquinquagintillion', + 174 => 'septenquinquagintillion', + 177 => 'octoquinquagintillion', + 180 => 'novemquinquagintillion', + 183 => 'sexagintillion', + 186 => 'unsexagintillion', + 189 => 'duosexagintillion', + 192 => 'tresexagintillion', + 195 => 'quattuorsexagintillion', + 198 => 'quinsexagintillion', + 201 => 'sexsexagintillion', + 204 => 'septensexagintillion', + 207 => 'octosexagintillion', + 210 => 'novemsexagintillion', + 213 => 'septuagintillion', + 216 => 'unseptuagintillion', + 219 => 'duoseptuagintillion', + 222 => 'treseptuagintillion', + 225 => 'quattuorseptuagintillion', + 228 => 'quinseptuagintillion', + 231 => 'sexseptuagintillion', + 234 => 'septenseptuagintillion', + 237 => 'octoseptuagintillion', + 240 => 'novemseptuagintillion', + 243 => 'octogintillion', + 246 => 'unoctogintillion', + 249 => 'duooctogintillion', + 252 => 'treoctogintillion', + 255 => 'quattuoroctogintillion', + 258 => 'quinoctogintillion', + 261 => 'sexoctogintillion', + 264 => 'septoctogintillion', + 267 => 'octooctogintillion', + 270 => 'novemoctogintillion', + 273 => 'nonagintillion', + 276 => 'unnonagintillion', + 279 => 'duononagintillion', + 282 => 'trenonagintillion', + 285 => 'quattuornonagintillion', + 288 => 'quinnonagintillion', + 291 => 'sexnonagintillion', + 294 => 'septennonagintillion', + 297 => 'octononagintillion', + 300 => 'novemnonagintillion', + 303 => 'centillion' + ); + // }}} + + // {{{ _splitNumber() + + /** + * Split a number to groups of three-digit numbers. + * + * @param mixed $num An integer or its string representation + * that need to be split + * + * @return array Groups of three-digit numbers. + * + * @access private + * @author Kouber Saparev + * @since PHP 4.2.3 + */ + + function _splitNumber($num) + { + if (is_string($num)) { + $ret = array(); + $strlen = strlen($num); + $first = substr($num, 0, $strlen%3); + preg_match_all('/\d{3}/', substr($num, $strlen%3, $strlen), $m); + $ret =& $m[0]; + if ($first) array_unshift($ret, $first); + return $ret; + } + else + return explode(' ', number_format($num, 0, '', ' ')); // a faster version for integers + } + // }}} + + // {{{ _showDigitsGroup() + + /** + * Converts a three-digit number to its word representation + * in French language. + * + * @param integer $num An integer between 1 and 999 inclusive. + * + * @param boolean $last A flag, that determines if it is the last group of digits - + * this is used to accord the plural suffix of the "hundreds". + * Example: 200 = "deux cents", but 200000 = "deux cent mille". + * + * + * @return string The words for the given number. + * + * @access private + * @author Kouber Saparev + */ + function _showDigitsGroup($num, $last = false) + { + $ret = ''; + + // extract the value of each digit from the three-digit number + $e = $num%10; // ones + $d = ($num-$e)%100/10; // tens + $s = ($num-$d*10-$e)%1000/100; // hundreds + + // process the "hundreds" digit. + if ($s) { + if ($s>1) { + $ret .= $this->_digits[$s].$this->_sep.$this->_misc_numbers[100]; + if ($last && !$e && !$d) { + $ret .= $this->_plural; + } + } else { + $ret .= $this->_misc_numbers[100]; + } + $ret .= $this->_sep; + } + + // process the "tens" digit, and optionally the "ones" digit. + if ($d) { + // in the case of 1, the "ones" digit also must be processed + if ($d==1) { + if ($e<=6) { + $ret .= $this->_misc_numbers[10+$e]; + } else { + $ret .= $this->_misc_numbers[10].'-'.$this->_digits[$e]; + } + $e = 0; + } elseif ($d>5) { + if ($d<8) { + $ret .= $this->_misc_numbers[60]; + $resto = $d*10+$e-60; + if ($e==1) { + $ret .= $this->_sep.$this->_and.$this->_sep; + } + elseif ($resto) { + $ret .= $this->_dash; + } + + if ($resto) { + $ret .= $this->_showDigitsGroup($resto); + } + $e = 0; + } else { + $ret .= $this->_digits[4].$this->_dash.$this->_misc_numbers[20]; + $resto = $d*10+$e-80; + if ($resto) { + $ret .= $this->_dash; + $ret .= $this->_showDigitsGroup($resto); + $e = 0; + } else { + $ret .= $this->_plural; + } + } + } else { + $ret .= $this->_misc_numbers[$d*10]; + } + } + + // process the "ones" digit + if ($e) { + if ($d) { + if ($e==1) { + $ret .= $this->_sep.$this->_and.$this->_sep; + } else { + $ret .= $this->_dash; + } + } + $ret .= $this->_digits[$e]; + } + + // strip excessive separators + $ret = rtrim($ret, $this->_sep); + + return $ret; + } + // }}} + + // {{{ toWords() + + /** + * Converts a number to its word representation + * in French language. + * + * @param integer $num An integer (or its string representation) between 9.99*-10^302 + * and 9.99*10^302 (999 centillions) that need to be converted to words + * + * @return string The corresponding word representation + * + * @access public + * @author Kouber Saparev + */ + function toWords($num = 0) + { + $ret = ''; + + // check if $num is a valid non-zero number + if (!$num || preg_match('/^-?0+$/', $num) || !preg_match('/^-?\d+$/', $num)) return $this->_zero; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_minus . $this->_sep; + $num = substr($num, 1); + } + + // if the absolute value is greater than 9.99*10^302, return infinity + if (strlen($num)>306) { + return $ret . $this->_infinity; + } + + // strip excessive zero signs + $num = ltrim($num, '0'); + + // split $num to groups of three-digit numbers + $num_groups = $this->_splitNumber($num); + + $sizeof_numgroups = count($num_groups); + + foreach ($num_groups as $i=>$number) { + // what is the corresponding exponent for the current group + $pow = $sizeof_numgroups-$i; + + // skip processment for empty groups + if ($number!='000') { + if ($number!=1 || $pow!=2) { + $ret .= $this->_showDigitsGroup($number, $i+1==$sizeof_numgroups).$this->_sep; + } + $ret .= $this->_exponent[($pow-1)*3]; + if ($pow>2 && $number>1) { + $ret .= $this->_plural; + } + $ret .= $this->_sep; + } + } + + return rtrim($ret, $this->_sep); + } + // }}} +} +?> diff --git a/Numbers/Words/lang.fr_BE.php b/Numbers/Words/lang.fr_BE.php new file mode 100644 index 0000000..42bb1aa --- /dev/null +++ b/Numbers/Words/lang.fr_BE.php @@ -0,0 +1,417 @@ + | +// | Philippe Bajoit | +// +----------------------------------------------------------------------+ +// +// $Id: lang.fr_BE.php,v 1.1 2005/01/11 14:30:34 makler Exp $ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into French (Belgium). + * + * @author Kouber Saparev + * @package Numbers_Words + */ +class Numbers_Words_fr_BE extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name. + * @var string + * @access public + */ + var $locale = 'fr'; + + /** + * Language name in English. + * @var string + * @access public + */ + var $lang = 'French'; + + /** + * Native language name. + * @var string + * @access public + */ + var $lang_native = 'Français'; + + /** + * The words for some numbers. + * @var string + * @access private + */ + var $_misc_numbers = array( + 10=>'dix', // 10 + 'onze', // 11 + 'douze', // 12 + 'treize', // 13 + 'quatorze', // 14 + 'quinze', // 15 + 'seize', // 16 + 20=>'vingt', // 20 + 30=>'trente', // 30 + 40=>'quarante', // 40 + 50=>'cinquante',// 50 + 60=>'soixante', // 60 + 70=>'septante', // 70 + 80=>'quatre-vingt', // 80 + 90=>'nonante', // 90 + 100=>'cent' // 100 + ); + + + /** + * The words for digits (except zero). + * @var string + * @access private + */ + var $_digits = array(1=>"un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf"); + + /** + * The word for zero. + * @var string + * @access private + */ + var $_zero = 'zéro'; + + /** + * The word for infinity. + * @var string + * @access private + */ + var $_infinity = 'infini'; + + /** + * The word for the "and" language construct. + * @var string + * @access private + */ + var $_and = 'et'; + + /** + * The word separator. + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The dash liaison. + * @var string + * @access private + */ + var $_dash = '-'; + + /** + * The word for the minus sign. + * @var string + * @access private + */ + var $_minus = 'moins'; // minus sign + + /** + * The plural suffix (except for hundred). + * @var string + * @access private + */ + var $_plural = 's'; // plural suffix + + /** + * The suffixes for exponents (singular). + * @var array + * @access private + */ + var $_exponent = array( + 0 => '', + 3 => 'mille', + 6 => 'million', + 9 => 'milliard', + 12 => 'trillion', + 15 => 'quadrillion', + 18 => 'quintillion', + 21 => 'sextillion', + 24 => 'septillion', + 27 => 'octillion', + 30 => 'nonillion', + 33 => 'decillion', + 36 => 'undecillion', + 39 => 'duodecillion', + 42 => 'tredecillion', + 45 => 'quattuordecillion', + 48 => 'quindecillion', + 51 => 'sexdecillion', + 54 => 'septendecillion', + 57 => 'octodecillion', + 60 => 'novemdecillion', + 63 => 'vigintillion', + 66 => 'unvigintillion', + 69 => 'duovigintillion', + 72 => 'trevigintillion', + 75 => 'quattuorvigintillion', + 78 => 'quinvigintillion', + 81 => 'sexvigintillion', + 84 => 'septenvigintillion', + 87 => 'octovigintillion', + 90 => 'novemvigintillion', + 93 => 'trigintillion', + 96 => 'untrigintillion', + 99 => 'duotrigintillion', + 102 => 'trestrigintillion', + 105 => 'quattuortrigintillion', + 108 => 'quintrigintillion', + 111 => 'sextrigintillion', + 114 => 'septentrigintillion', + 117 => 'octotrigintillion', + 120 => 'novemtrigintillion', + 123 => 'quadragintillion', + 126 => 'unquadragintillion', + 129 => 'duoquadragintillion', + 132 => 'trequadragintillion', + 135 => 'quattuorquadragintillion', + 138 => 'quinquadragintillion', + 141 => 'sexquadragintillion', + 144 => 'septenquadragintillion', + 147 => 'octoquadragintillion', + 150 => 'novemquadragintillion', + 153 => 'quinquagintillion', + 156 => 'unquinquagintillion', + 159 => 'duoquinquagintillion', + 162 => 'trequinquagintillion', + 165 => 'quattuorquinquagintillion', + 168 => 'quinquinquagintillion', + 171 => 'sexquinquagintillion', + 174 => 'septenquinquagintillion', + 177 => 'octoquinquagintillion', + 180 => 'novemquinquagintillion', + 183 => 'sexagintillion', + 186 => 'unsexagintillion', + 189 => 'duosexagintillion', + 192 => 'tresexagintillion', + 195 => 'quattuorsexagintillion', + 198 => 'quinsexagintillion', + 201 => 'sexsexagintillion', + 204 => 'septensexagintillion', + 207 => 'octosexagintillion', + 210 => 'novemsexagintillion', + 213 => 'septuagintillion', + 216 => 'unseptuagintillion', + 219 => 'duoseptuagintillion', + 222 => 'treseptuagintillion', + 225 => 'quattuorseptuagintillion', + 228 => 'quinseptuagintillion', + 231 => 'sexseptuagintillion', + 234 => 'septenseptuagintillion', + 237 => 'octoseptuagintillion', + 240 => 'novemseptuagintillion', + 243 => 'octogintillion', + 246 => 'unoctogintillion', + 249 => 'duooctogintillion', + 252 => 'treoctogintillion', + 255 => 'quattuoroctogintillion', + 258 => 'quinoctogintillion', + 261 => 'sexoctogintillion', + 264 => 'septoctogintillion', + 267 => 'octooctogintillion', + 270 => 'novemoctogintillion', + 273 => 'nonagintillion', + 276 => 'unnonagintillion', + 279 => 'duononagintillion', + 282 => 'trenonagintillion', + 285 => 'quattuornonagintillion', + 288 => 'quinnonagintillion', + 291 => 'sexnonagintillion', + 294 => 'septennonagintillion', + 297 => 'octononagintillion', + 300 => 'novemnonagintillion', + 303 => 'centillion' + ); + // }}} + + // {{{ _splitNumber() + + /** + * Split a number to groups of three-digit numbers. + * + * @param mixed $num An integer or its string representation + * that need to be split + * + * @return array Groups of three-digit numbers. + * + * @access private + * @author Kouber Saparev + * @since PHP 4.2.3 + */ + + function _splitNumber($num) + { + if (is_string($num)) { + $ret = array(); + $strlen = strlen($num); + $first = substr($num, 0, $strlen%3); + preg_match_all('/\d{3}/', substr($num, $strlen%3, $strlen), $m); + $ret =& $m[0]; + if ($first) array_unshift($ret, $first); + return $ret; + } + else + return explode(' ', number_format($num, 0, '', ' ')); // a faster version for integers + } + // }}} + + // {{{ _showDigitsGroup() + + /** + * Converts a three-digit number to its word representation + * in French language. + * + * @param integer $num An integer between 1 and 999 inclusive. + * + * @param boolean $last A flag, that determines if it is the last group of digits - + * this is used to accord the plural suffix of the "hundreds". + * Example: 200 = "deux cents", but 200000 = "deux cent mille". + * + * + * @return string The words for the given number. + * + * @access private + * @author Kouber Saparev + */ + function _showDigitsGroup($num, $last = false) + { + $ret = ''; + + // extract the value of each digit from the three-digit number + $e = $num%10; // ones + $d = ($num-$e)%100/10; // tens + $s = ($num-$d*10-$e)%1000/100; // hundreds + + // process the "hundreds" digit. + if ($s) { + if ($s>1) { + $ret .= $this->_digits[$s].$this->_sep.$this->_misc_numbers[100]; + if ($last && !$e && !$d) { + $ret .= $this->_plural; + } + } else { + $ret .= $this->_misc_numbers[100]; + } + $ret .= $this->_sep; + } + + // process the "tens" digit, and optionally the "ones" digit. + if ($d) { + // in the case of 1, the "ones" digit also must be processed + if ($d==1) { + if ($e<=6) { + $ret .= $this->_misc_numbers[10+$e]; + } else { + $ret .= $this->_misc_numbers[10].'-'.$this->_digits[$e]; + } + $e = 0; + } else { + $ret .= $this->_misc_numbers[$d*10]; + } + } + + // process the "ones" digit + if ($e) { + if ($d) { + if ($e==1) { + $ret .= $this->_sep.$this->_and.$this->_sep; + } else { + $ret .= $this->_dash; + } + } + $ret .= $this->_digits[$e]; + } + + // strip excessive separators + $ret = rtrim($ret, $this->_sep); + + return $ret; + } + // }}} + + // {{{ toWords() + + /** + * Converts a number to its word representation + * in French language. + * + * @param integer $num An integer (or its string representation) between 9.99*-10^302 + * and 9.99*10^302 (999 centillions) that need to be converted to words + * + * @return string The corresponding word representation + * + * @access public + * @author Kouber Saparev + */ + function toWords($num = 0) + { + $ret = ''; + + // check if $num is a valid non-zero number + if (!$num || preg_match('/^-?0+$/', $num) || !preg_match('/^-?\d+$/', $num)) return $this->_zero; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_minus . $this->_sep; + $num = substr($num, 1); + } + + // if the absolute value is greater than 9.99*10^302, return infinity + if (strlen($num)>306) { + return $ret . $this->_infinity; + } + + // strip excessive zero signs + $num = ltrim($num, '0'); + + // split $num to groups of three-digit numbers + $num_groups = $this->_splitNumber($num); + + $sizeof_numgroups = count($num_groups); + + foreach ($num_groups as $i=>$number) { + // what is the corresponding exponent for the current group + $pow = $sizeof_numgroups-$i; + + // skip processment for empty groups + if ($number!='000') { + if ($number!=1 || $pow!=2) { + $ret .= $this->_showDigitsGroup($number, $i+1==$sizeof_numgroups).$this->_sep; + } + $ret .= $this->_exponent[($pow-1)*3]; + if ($pow>2 && $number>1) { + $ret .= $this->_plural; + } + $ret .= $this->_sep; + } + } + + return rtrim($ret, $this->_sep); + } + // }}} +} +?> diff --git a/Numbers/Words/lang.he.php b/Numbers/Words/lang.he.php new file mode 100644 index 0000000..732f8d7 --- /dev/null +++ b/Numbers/Words/lang.he.php @@ -0,0 +1,506 @@ + array(''), + 3 => array('×לפי×'), + 6 => array('מיליון'), + 9 => array('ביליון'), + 12 => array('טריליון'), + 15 => array('קוודריליון'), + 18 => array('קווינטיליון'), + 21 => array('sextillion'), + 24 => array('septillion'), + 27 => array('octillion'), + 30 => array('nonillion'), + 33 => array('decillion'), + 36 => array('undecillion'), + 39 => array('duodecillion'), + 42 => array('tredecillion'), + 45 => array('quattuordecillion'), + 48 => array('quindecillion'), + 51 => array('sexdecillion'), + 54 => array('septendecillion'), + 57 => array('octodecillion'), + 60 => array('novemdecillion'), + 63 => array('vigintillion'), + 66 => array('unvigintillion'), + 69 => array('duovigintillion'), + 72 => array('trevigintillion'), + 75 => array('quattuorvigintillion'), + 78 => array('quinvigintillion'), + 81 => array('sexvigintillion'), + 84 => array('septenvigintillion'), + 87 => array('octovigintillion'), + 90 => array('novemvigintillion'), + 93 => array('trigintillion'), + 96 => array('untrigintillion'), + 99 => array('duotrigintillion'), + // 100 => array('googol') - not latin name + // 10^googol = 1 googolplex + 102 => array('trestrigintillion'), + 105 => array('quattuortrigintillion'), + 108 => array('quintrigintillion'), + 111 => array('sextrigintillion'), + 114 => array('septentrigintillion'), + 117 => array('octotrigintillion'), + 120 => array('novemtrigintillion'), + 123 => array('quadragintillion'), + 126 => array('unquadragintillion'), + 129 => array('duoquadragintillion'), + 132 => array('trequadragintillion'), + 135 => array('quattuorquadragintillion'), + 138 => array('quinquadragintillion'), + 141 => array('sexquadragintillion'), + 144 => array('septenquadragintillion'), + 147 => array('octoquadragintillion'), + 150 => array('novemquadragintillion'), + 153 => array('quinquagintillion'), + 156 => array('unquinquagintillion'), + 159 => array('duoquinquagintillion'), + 162 => array('trequinquagintillion'), + 165 => array('quattuorquinquagintillion'), + 168 => array('quinquinquagintillion'), + 171 => array('sexquinquagintillion'), + 174 => array('septenquinquagintillion'), + 177 => array('octoquinquagintillion'), + 180 => array('novemquinquagintillion'), + 183 => array('sexagintillion'), + 186 => array('unsexagintillion'), + 189 => array('duosexagintillion'), + 192 => array('tresexagintillion'), + 195 => array('quattuorsexagintillion'), + 198 => array('quinsexagintillion'), + 201 => array('sexsexagintillion'), + 204 => array('septensexagintillion'), + 207 => array('octosexagintillion'), + 210 => array('novemsexagintillion'), + 213 => array('septuagintillion'), + 216 => array('unseptuagintillion'), + 219 => array('duoseptuagintillion'), + 222 => array('treseptuagintillion'), + 225 => array('quattuorseptuagintillion'), + 228 => array('quinseptuagintillion'), + 231 => array('sexseptuagintillion'), + 234 => array('septenseptuagintillion'), + 237 => array('octoseptuagintillion'), + 240 => array('novemseptuagintillion'), + 243 => array('octogintillion'), + 246 => array('unoctogintillion'), + 249 => array('duooctogintillion'), + 252 => array('treoctogintillion'), + 255 => array('quattuoroctogintillion'), + 258 => array('quinoctogintillion'), + 261 => array('sexoctogintillion'), + 264 => array('septoctogintillion'), + 267 => array('octooctogintillion'), + 270 => array('novemoctogintillion'), + 273 => array('nonagintillion'), + 276 => array('unnonagintillion'), + 279 => array('duononagintillion'), + 282 => array('trenonagintillion'), + 285 => array('quattuornonagintillion'), + 288 => array('quinnonagintillion'), + 291 => array('sexnonagintillion'), + 294 => array('septennonagintillion'), + 297 => array('octononagintillion'), + 300 => array('novemnonagintillion'), + 303 => array('centillion'), + 309 => array('duocentillion'), + 312 => array('trecentillion'), + 366 => array('primo-vigesimo-centillion'), + 402 => array('trestrigintacentillion'), + 603 => array('ducentillion'), + 624 => array('septenducentillion'), + // bug on a earthlink page: 903 => array('trecentillion'), + 2421 => array('sexoctingentillion'), + 3003 => array('millillion'), + 3000003 => array('milli-millillion') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => '×פס', '×חד', 'שניי×', 'שלושה', '×רבעה', + 'חמישה', 'שישה', 'שבעה', 'שמונה', 'תשעה' + ); + + var $_digits_ten = array( + 2 => 'עשרי×', 'שלושי×', '×רבעי×', 'חמישי×', + 'שישי×', 'שבעי×', 'שמוני×', 'תשעי×' + ); + + var $_digits_female = array( + 0 => '×פס', '×חת', 'שתיי×', 'שלוש', '×רבע', + 'חמש', 'שש', 'שבע', 'שמונה', 'תשע' + ); + + var $digits_hundreds = array( + 0 => '', 'מ××”', 'שני-מ×ות', 'שלוש-מ×ות', '×רבע-מ×ות', + 'חמש-מ×ות', 'שש-מ×ות', 'שבע-מ×ות', 'שמונה-מ×ות', 'תשע-מ×ות' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + var $_sep_hundred = '-'; + var $_sep_ten = ' ו'; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek'), array('qindarka')), + 'AUD' => array(array('Australian dollar'), array('cent')), + 'BAM' => array(array('convertible marka'), array('fenig')), + 'BGN' => array(array('lev'), array('stotinka')), + 'BRL' => array(array('real'), array('centavos')), + 'BYR' => array(array('Belarussian rouble'), array('kopiejka')), + 'CAD' => array(array('Canadian dollar'), array('cent')), + 'CHF' => array(array('Swiss franc'), array('rapp')), + 'CYP' => array(array('Cypriot pound'), array('cent')), + 'CZK' => array(array('Czech koruna'), array('halerz')), + 'DKK' => array(array('Danish krone'), array('ore')), + 'EEK' => array(array('kroon'), array('senti')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pound', 'pounds'), array('pence', 'pence')), + 'HKD' => array(array('Hong Kong dollar'), array('cent')), + 'HRK' => array(array('Croatian kuna'), array('lipa')), + 'HUF' => array(array('forint'), array('filler')), + 'ILS' => array(array('new sheqel','new sheqels'), array('agora','agorot')), + 'ISK' => array(array('Icelandic kr�na'), array('aurar')), + 'JPY' => array(array('yen'), array('sen')), + 'LTL' => array(array('litas'), array('cent')), + 'LVL' => array(array('lat'), array('sentim')), + 'MKD' => array(array('Macedonian dinar'), array('deni')), + 'MTL' => array(array('Maltese lira'), array('centym')), + 'NIS' => array(array('×©×§×œ×™× ×—×“×©×™×'), array('nis')), // need more info if NIS or ILS + 'NOK' => array(array('Norwegian krone'), array('oere')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'ROL' => array(array('Romanian leu'), array('bani')), + 'RUB' => array(array('Russian Federation rouble'), array('kopiejka')), + 'SEK' => array(array('Swedish krona'), array('oere')), + 'SIT' => array(array('Tolar'), array('stotinia')), + 'SKK' => array(array('Slovak koruna'), array()), + 'TRL' => array(array('lira'), array('kuru�')), + 'UAH' => array(array('hryvna'), array('cent')), + 'USD' => array(array('dollar'), array('cent')), + 'YUM' => array(array('dinars'), array('para')), + 'ZAR' => array(array('rand'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'NIS'; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->digits_hundreds[$h]; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + + switch ($t) { + case 9: + case 8: + case 7: + case 6: + case 5: + case 4: + case 3: + case 2: + $ret .= $this->_sep . $this->_digits_ten[$t]; + break; + + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'עשר'; + break; + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . '-עשר'; + break; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + if ($t > 1) { + $ret .= $this -> _sep_ten . $this->_digits[$d]; + } else { + if (strlen($ret)>0) $ret .= $this->_sep . 'ו' . $this->_digits[$d]; + else $ret .= $this->_sep . $this->_digits[$d]; + } + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Piotr Klaban + * @since Numbers_Words 0.4 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + $ret .= $this->_sep . $curr_names[0][0] . ''; + } + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . ''; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + +} + +?> diff --git a/Numbers/Words/lang.hu_HU.php b/Numbers/Words/lang.hu_HU.php new file mode 100644 index 0000000..dd4f089 --- /dev/null +++ b/Numbers/Words/lang.hu_HU.php @@ -0,0 +1,402 @@ + (Original Class) | +// | Nils Homp (Hungarian version) | +// +----------------------------------------------------------------------+ +// +// $Id: lang.hu_HU.php,v 1.5 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Hungarian language. +// NOTE: toCurrency() was not localized and is from the en_US class. +// + +/** + * Class for translating numbers into Hungarian. + * + * @author Nils Homp + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Hungarian. + * + * @author Nils Homp + * @package Numbers_Words + */ +class Numbers_Words_hu_HU extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'hu_HU'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Hungarian'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Magyar'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'Minusz'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names based on: + * http://mek.oszk.hu/adatbazis/lexikon/phplex/lexikon/d/kisokos/186.html + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('ezer'), + 6 => array('millió'), + 9 => array('milliárd'), + 12 => array('billió'), + 15 => array('billiárd'), + 18 => array('trillió'), + 21 => array('trilliárd'), + 24 => array('kvadrillió'), + 27 => array('kvadrilliárd'), + 30 => array('kvintillió'), + 33 => array('kvintilliárd'), + 36 => array('szextillió'), + 39 => array('szextilliárd'), + 42 => array('szeptillió'), + 45 => array('szeptilliárd'), + 48 => array('oktillió'), + 51 => array('oktilliárd'), + 54 => array('nonillió'), + 57 => array('nonilliárd'), + 60 => array('decillió'), + 63 => array('decilliárd'), + 600 => array('centillió') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'nulla', 'egy', 'kettõ', 'három', 'négy', + 'öt', 'hat', 'hét', 'nyolc', 'kilenc' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ''; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek'), array('qindarka')), + 'AUD' => array(array('Australian dollar'), array('cent')), + 'BAM' => array(array('convertible marka'), array('fenig')), + 'BGN' => array(array('lev'), array('stotinka')), + 'BRL' => array(array('real'), array('centavos')), + 'BYR' => array(array('Belarussian rouble'), array('kopiejka')), + 'CAD' => array(array('Canadian dollar'), array('cent')), + 'CHF' => array(array('Swiss franc'), array('rapp')), + 'CYP' => array(array('Cypriot pound'), array('cent')), + 'CZK' => array(array('Czech koruna'), array('halerz')), + 'DKK' => array(array('Danish krone'), array('ore')), + 'EEK' => array(array('kroon'), array('senti')), + 'EUR' => array(array('euro'), array('euro-cent')), + 'GBP' => array(array('pound', 'pounds'), array('pence', 'pence')), + 'HKD' => array(array('Hong Kong dollar'), array('cent')), + 'HRK' => array(array('Croatian kuna'), array('lipa')), + 'HUF' => array(array('forint'), array('filler')), + 'ILS' => array(array('new sheqel','new sheqels'), array('agora','agorot')), + 'ISK' => array(array('Icelandic króna'), array('aurar')), + 'JPY' => array(array('yen'), array('sen')), + 'LTL' => array(array('litas'), array('cent')), + 'LVL' => array(array('lat'), array('sentim')), + 'MKD' => array(array('Macedonian dinar'), array('deni')), + 'MTL' => array(array('Maltese lira'), array('centym')), + 'NOK' => array(array('Norwegian krone'), array('oere')), + 'PLN' => array(array('zloty', 'zlotys'), array('grosz')), + 'ROL' => array(array('Romanian leu'), array('bani')), + 'RUB' => array(array('Russian Federation rouble'), array('kopiejka')), + 'SEK' => array(array('Swedish krona'), array('oere')), + 'SIT' => array(array('Tolar'), array('stotinia')), + 'SKK' => array(array('Slovak koruna'), array()), + 'TRL' => array(array('lira'), array('kuruþ')), + 'UAH' => array(array('hryvna'), array('cent')), + 'USD' => array(array('dollar'), array('cent')), + 'YUM' => array(array('dinars'), array('para')), + 'ZAR' => array(array('rand'), array('cent')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'HUF'; // forint + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in the Hungarian language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Nils Homp + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'száz'; + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 5: + case 4: + $ret .= $this->_sep . $this->_digits[$t] . 'ven'; + break; + case 8: + case 6: + $ret .= $this->_sep . $this->_digits[$t] . 'van'; + break; + case 7: + $ret .= $this->_sep . 'hetven'; + break; + case 3: + $ret .= $this->_sep . 'harminc'; + break; + case 2: + switch ($d) { + case 0: + $ret .= $this->_sep . 'húsz'; + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . 'húszon'; + break; + } + break; + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'tíz'; + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . 'tizen'; + break; + } + break; + } + + if ($d > 0) { // add digits only in <0> and <1,inf) + $ret .= $this->_sep . $this->_digits[$d]; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in English language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Piotr Klaban + * @since Numbers_Words 0.4 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + $ret .= $this->_sep . $curr_names[0][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[0][0]; + } + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + return $ret; + } + // }}} + +} + +?> diff --git a/Numbers/Words/lang.id.php b/Numbers/Words/lang.id.php new file mode 100644 index 0000000..3dc2e27 --- /dev/null +++ b/Numbers/Words/lang.id.php @@ -0,0 +1,277 @@ +, Arif Rifai Dwiyanto | +// +----------------------------------------------------------------------+ +// +// $Id: lang.id.php,v 1.1 2004/09/02 11:07:59 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Indonesian language. +// + +require_once("PEAR.php"); +require_once("Numbers/Words.php"); + +/** +* Class for translating numbers into Indonesian. +* +* @author Ernas M. Jamil, Arif Rifai Dwiyanto +*/ +class Numbers_Words_id extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + */ + var $locale = 'id'; + + /** + * Language name in English + * @var string + */ + var $lang = 'Indonesia Language'; + + /** + * Native language name + * @var string + */ + var $lang_native = 'Bahasa Indonesia'; + + /** + * The word for the minus sign + * @var string + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://www.users.dircon.co.uk/~shaunf/shaun/numbers/millions.htm + * @var array + */ + var $_exponent = array( + 0 => array(''), + 3 => array('ribu'), + 6 => array('juta'), + 9 => array('milyar'), + 12 => array('trilyun'), + 24 => array('quadrillion'), + 30 => array('quintillion'), + 36 => array('sextillion'), + 42 => array('septillion'), + 48 => array('octillion'), + 54 => array('nonillion'), + 60 => array('decillion'), + 66 => array('undecillion'), + 72 => array('duodecillion'), + 78 => array('tredecillion'), + 84 => array('quattuordecillion'), + 90 => array('quindecillion'), + 96 => array('sexdecillion'), + 102 => array('septendecillion'), + 108 => array('octodecillion'), + 114 => array('novemdecillion'), + 120 => array('vigintillion'), + 192 => array('duotrigintillion'), + 600 => array('centillion') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + */ + var $_digits = array( + 0 => 'nol', 'satu', 'dua', 'tiga', 'empat', + 'lima', 'enam', 'tujuh', 'delapan', 'sembilan' + ); + + /** + * The word separator + * @var string + */ + var $_sep = ' '; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Indonesian language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Ernas M. Jamil + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 4) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = $th = 0; + + switch(strlen($num)) { + case 4: + $th = (int)substr($num,-4,1); + + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($th) { + if ($th==1) + $ret .= $this->_sep . 'seribu'; + else + $ret .= $this->_sep . $this->_digits[$th] . $this->_sep . 'ribu'; + } + + if ($h) { + if ($h==1) + $ret .= $this->_sep . 'seratus'; + else + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'ratus'; + + // in English only - add ' and' for [1-9]01..[1-9]99 + // (also for 1001..1099, 10001..10099 but it is harder) + // for now it is switched off, maybe some language purists + // can force me to enable it, or to remove it completely + // if (($t + $d) > 0) + // $ret .= $this->_sep . 'and'; + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 8: + case 7: + case 6: + case 5: + case 4: + case 3: + case 2: + $ret .= $this->_sep . $this->_digits[$t] . ' puluh'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'sepuluh'; + break; + + case 1: + $ret .= $this->_sep . 'sebelas'; + break; + + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . ' belas'; + break; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + if ($t > 1) { + $ret .= ' ' . $this->_digits[$d]; + } else { + $ret .= $this->_sep . $this->_digits[$d]; + } + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.it_IT.php b/Numbers/Words/lang.it_IT.php new file mode 100644 index 0000000..50694f7 --- /dev/null +++ b/Numbers/Words/lang.it_IT.php @@ -0,0 +1,348 @@ + + * @author Davide Caironi + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Italian. + * It supports up to quadrilions + * + * @author Filippo Beltramini + * @author Davide Caironi + * @package Numbers_Words + */ +class Numbers_Words_it_IT extends Numbers_Words +{ + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'it_IT'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Italian'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Italiano'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'meno '; + + /** + * The sufixes for exponents (singular and plural) + * @var array + * @access private + */ + var $_exponent = array( + 0 => array('',''), + 3 => array('mille','mila'), + 6 => array('milione','miloni'), + 12 => array('miliardo','miliardi'), + 18 => array('trillone','trilloni'), + 24 => array('quadrilione','quadrilioni'), + ); + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'zero', 'uno', 'due', 'tre', 'quattro', + 'cinque', 'sei', 'sette', 'otto', 'nove' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ''; + // }}} + // {{{ toWords() + /** + * Converts a number to its word representation + * in italiano. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that should be converted to a words representation + * @param integer $power The power of ten for the rest of the number to the right. + * For example toWords(12,3) should give "doce mil". + * Optional, defaults to 0. + * @return string The corresponding word representation + * + * @access private + * @author Filippo Beltramini + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0) + { + // The return string; + $ret = ''; + + // add a the word for the minus sign if necessary + if (substr($num, 0, 1) == '-') + { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + + // strip excessive zero signs + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 6) + { + $current_power = 6; + // check for highest power + if (isset($this->_exponent[$power])) + { + // convert the number above the first 6 digits + // with it's corresponding $power. + $snum = substr($num, 0, -6); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $ret .= $this->toWords($snum, $power + 6); + } + } + $num = substr($num, -6); + if ($num == 0) { + return $ret; + } + } + elseif ($num == 0 || $num == '') { + return(' '.$this->_digits[0].' '); + $current_power = strlen($num); + } + else { + $current_power = strlen($num); + } + + // See if we need "thousands" + $thousands = floor($num / 1000); + if ($thousands == 1) { + $ret .= $this->_sep . 'mille' . $this->_sep; + } + elseif ($thousands > 1) { + $ret .= $this->toWords($thousands, 3) . $this->_sep;//. 'mil' . $this->_sep; + } + + // values for digits, tens and hundreds + $h = floor(($num / 100) % 10); + $t = floor(($num / 10) % 10); + $d = floor($num % 10); + + // centinaia: duecento, trecento, etc... + switch ($h) + { + case 1: + if (($d == 0) and ($t == 0)) { // is it's '100' use 'cien' + $ret .= $this->_sep . 'cento'; + } + else { + $ret .= $this->_sep . 'cento'; + } + break; + case 2: + case 3: + case 4: + case 6: + case 8: + $ret .= $this->_sep . $this->_digits[$h] . 'cento'; + break; + case 5: + $ret .= $this->_sep . 'cinquecento'; + break; + case 7: + $ret .= $this->_sep . 'settecento'; + break; + case 9: + $ret .= $this->_sep . 'novecento'; + break; + } + + // decine: venti trenta, etc... + switch ($t) + { + case 9: + $ret .= $this->_sep . 'novanta'; + break; + + case 8: + $ret .= $this->_sep . 'ottanta'; + break; + + case 7: + $ret .= $this->_sep . 'settanta'; + break; + + case 6: + $ret .= $this->_sep . 'sessanta'; + break; + + case 5: + $ret .= $this->_sep . 'cinquanta'; + break; + + case 4: + $ret .= $this->_sep . 'quaranta'; + break; + + case 3: + $ret .= $this->_sep . 'trenta'; + break; + + case 2: + if ($d == 0) { + $ret .= $this->_sep . 'venti'; + } + else { + if (($power > 0) and ($d == 1)) { + $ret .= $this->_sep . 'ventuno'; + } + else { + $ret .= $this->_sep . 'venti' . $this->_digits[$d]; + } + } + break; + + case 1: + switch ($d) + { + case 0: + $ret .= $this->_sep . 'dieci'; + break; + + case 1: + $ret .= $this->_sep . 'undici'; + break; + + case 2: + $ret .= $this->_sep . 'dodici'; + break; + + case 3: + $ret .= $this->_sep . 'tredici'; + break; + + case 4: + $ret .= $this->_sep . 'quattordici'; + break; + + case 5: + $ret .= $this->_sep . 'quindici'; + break; + + case 6: + $ret .= $this->_sep . 'sedici'; + break; + + case 7: + $ret .= $this->_sep . 'diciassette'; + break; + + case 8: + $ret .= $this->_sep . 'diciotto'; + break; + + case 9: + $ret .= $this->_sep . 'diciannove'; + break; + } + break; + } + + // add digits only if it is a multiple of 10 and not 1x or 2x + if (($t != 1) and ($t != 2) and ($d > 0)) + { + if($t != 0) // don't add 'e' for numbers below 10 + { + // use 'un' instead of 'uno' when there is a suffix ('mila', 'milloni', etc...) + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.' e un'; + } + else { + $ret .= $this->_sep.''.$this->_digits[$d]; + } + } + else { + if(($power > 0) and ($d == 1)) { + $ret .= $this->_sep.'un '; + } + else { + $ret .= $this->_sep.$this->_digits[$d]; + } + } + } + + if ($power > 0) + { + if (isset($this->_exponent[$power])) { + $lev = $this->_exponent[$power]; + } + + if (!isset($lev) || !is_array($lev)) { + return null; + } + + // if it's only one use the singular suffix + if (($d == 1) and ($t == 0) and ($h == 0)) { + $suffix = $lev[0]; + } + else { + $suffix = $lev[1]; + } + if ($num != 0) { + $ret .= $this->_sep . $suffix; + } + } + + return $ret; + } + // }}} +} +?> diff --git a/Numbers/Words/lang.lt.php b/Numbers/Words/lang.lt.php new file mode 100644 index 0000000..810f8d2 --- /dev/null +++ b/Numbers/Words/lang.lt.php @@ -0,0 +1,310 @@ + array(''), + 3 => array('tûkstantis','tûkstanèiai','tûkstanèiø'), + 6 => array('milijonas','milijonai','milijonø'), + 9 => array('bilijonas','bilijonai','bilijonø'), + 12 => array('trilijonas','trilijonai','trilijonø'), + 15 => array('kvadrilijonas','kvadrilijonai','kvadrilijonø'), + 18 => array('kvintilijonas','kvintilijonai','kvintilijonø') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'nulis', 'vienas', 'du', 'trys', 'keturi', + 'penki', 'ðeði', 'septyni', 'aðtuoni', 'devyni' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'LTL'; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Lithuanian language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Laurynas Butkus + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ( $h > 1 ) + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'ðimtai'; + elseif ( $h ) + $ret .= $this->_sep . 'ðimtas'; + + // ten, twenty etc. + switch ($t) { + case 9: + $ret .= $this->_sep . 'devyniasdeðimt'; + break; + + case 8: + $ret .= $this->_sep . 'aðtuoniasdeðimt'; + break; + + case 7: + $ret .= $this->_sep . 'septyniasdeðimt'; + break; + + case 6: + $ret .= $this->_sep . 'ðeðiasdeðimt'; + break; + + case 5: + $ret .= $this->_sep . 'penkiasdeðimt'; + break; + + case 4: + $ret .= $this->_sep . 'keturiasdeðimt'; + break; + + case 3: + $ret .= $this->_sep . 'trisdeðimt'; + break; + + case 2: + $ret .= $this->_sep . 'dvideðimt'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'deðimt'; + break; + + case 1: + $ret .= $this->_sep . 'vienuolika'; + break; + + case 2: + $ret .= $this->_sep . 'dvylika'; + break; + + case 3: + $ret .= $this->_sep . 'trylika'; + break; + + case 4: + $ret .= $this->_sep . 'keturiolika'; + break; + + case 5: + $ret .= $this->_sep . 'penkiolika'; + break; + + case 6: + $ret .= $this->_sep . 'ðeðiolika'; + break; + + case 7: + $ret .= $this->_sep . 'septyniolika'; + break; + + case 8: + $ret .= $this->_sep . 'aðtuoniolika'; + break; + + case 9: + $ret .= $this->_sep . 'devyniolika'; + break; + + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + if ( $d > 1 || !$power || $t ) + $ret .= $this->_sep . $this->_digits[$d]; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + //echo " $t $d
"; + + if ( $t == 1 || ( $t > 0 && $d == 0 ) ) + $ret .= $this->_sep . $lev[2]; + elseif ( $d > 1 ) + $ret .= $this->_sep . $lev[1]; + else + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + +} + +?> diff --git a/Numbers/Words/lang.nl.php b/Numbers/Words/lang.nl.php new file mode 100644 index 0000000..d03da73 --- /dev/null +++ b/Numbers/Words/lang.nl.php @@ -0,0 +1,324 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.nl.php,v 1.1 2006/06/13 11:28:32 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Dutch language. +// + +/** + * + * Class for translating numbers into Dutch. + * @author Piotr Klaban + * @author WHAM van Dinter (for Dutch Translations) + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Dutch. + * + * @author Piotr Klaban + * @author WHAM van Dinter (for Dutch Translations) + * @package Numbers_Words + */ +class Numbers_Words_nl extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'nl'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Dutch'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Nederlands'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'Minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names partly based on: + * http://nl.wikipedia.org/wiki/Quadriljoen + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('Duizend','Duizend'), + 6 => array('Miljoen','Miljoen'), + 9 => array('Miljard','Miljard'), + 12 => array('Biljoen','Biljoen'), + 15 => array('Biljard','Biljard'), + 18 => array('Triljoen','Triljoen'), + 21 => array('Triljard','Triljard'), + 24 => array('Quadriljoen','Quadriljoen'), + 27 => array('Quadriljard','Quadriljard'), + 30 => array('Quintiljoen','Quintiljoen'), + 33 => array('Quintiljard','Quintiljard'), + 36 => array('Sextiljoen','Sextiljoen'), + 39 => array('Sextiljard','Sextiljard'), + 42 => array('Septiljoen','Septiljoen'), + 45 => array('Septiljard','Septiljard'), + 48 => array('Octiljoen','Octiljoen'), + 51 => array('Octiljard','Octiljard'), + 54 => array('Noniljoen','Noniljoen'), + 57 => array('Noniljard','Noniljard'), + 60 => array('Deciljoen','Deciljoen'), + 63 => array('Deciljard','Deciljard'), + 66 => array('Undeciljoen','Undeciljoen'), + 69 => array('Undeciljard','Undeciljard'), + 72 => array('Duodeciljoen','Duodeciljoen'), + 75 => array('Duodeciljard','Duodeciljard'), + 78 => array('Tredeciljoen','Tredeciljoen'), + 81 => array('Tredeciljard','Tredeciljard'), + 120 => array('Vigintiljoen','Vigintiljoen'), + 123 => array('Vigintiljard','Vigintiljard'), + 600 => array('Zentiljoen','Zentiljoen'), // oder Centillion + 603 => array('Zentiljardn','Zentiljard') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'nul', 'een', 'twee', 'drie', 'vier', + 'vijf', 'zes', 'zeven', 'acht', 'negen' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ''; + + /** + * The exponent word separator + * @var string + * @access private + */ + var $_sep2 = '-'; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Dutch language. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access private + * @author Piotr Klaban + * @author WHAM van Dinter + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'honderd'; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + if ($t > 0) { + $ret .= $this->_digits[$d] . 'en'; + } else { + $ret .= $this->_digits[$d]; + if ($d == 1) + if ($power == 0) { + $ret .= 's'; // fuer eins + } else { + if ($power != 3) { // tausend ausnehmen + $ret .= ''; // fuer eine + } + } + } + } + + // ten, twenty etc. + switch ($t) { + case 9: + case 8: + case 7: + case 6: + case 5: + $ret .= $this->_sep . $this->_digits[$t] . 'tig'; + break; + + case 4: + $ret .= $this->_sep . 'veertig'; + break; + + case 3: + $ret .= $this->_sep . 'dertig'; + break; + + case 2: + $ret .= $this->_sep . 'twintig'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'tien'; + break; + + case 1: + $ret .= $this->_sep . 'elf'; + break; + + case 2: + $ret .= $this->_sep . 'twaalf'; + break; + + case 3: + $ret .= $this->_sep . 'dertien'; + break; + + case 4: + $ret .= $this->_sep . 'veertien'; + break; + + case 5: + case 6: + case 7: + case 8: + case 9: + $ret .= $this->_sep . $this->_digits[$d] . 'tien'; + break; + + } + break; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + if ($power == 3) + $ret .= $this->_sep . $lev[0]; + elseif ($d == 1 && ($t+$h) == 0) + $ret .= $this->_sep2 . $lev[0] . $this->_sep2; + else + $ret .= $this->_sep2 . $lev[1] . $this->_sep2; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.pl.php b/Numbers/Words/lang.pl.php new file mode 100644 index 0000000..85b0200 --- /dev/null +++ b/Numbers/Words/lang.pl.php @@ -0,0 +1,520 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: lang.pl.php,v 1.4 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Polish. +// + +/** + * Class for translating numbers into Polish. + * + * @author Piotr Klaban + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Polish. + * + * @author Piotr Klaban + * @package Numbers_Words + */ +class Numbers_Words_pl extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'pl'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Polish'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'polski'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * Names based on: + * mathematical tables, my memory, and also: + * http://ux1.math.us.edu.pl/~szyjewski/FAQ/liczby/iony.htm + * @var array + * @access private + */ + var $_exponent = array( + // potêga dziesi±tki => liczba pojedyncza, podwójna, mnoga + 0 => array('','',''), + 3 => array('tysi±c','tysi±ce','tysiêcy'), + 6 => array('milion','miliony','milionów'), + 9 => array('miliard','miliardy','miliardów'), + 12 => array('bilion','biliony','bilionów'), + 15 => array('biliard','biliardy','biliardów'), + 18 => array('trylion','tryliony','trylionów'), + 21 => array('tryliard','tryliardy','tryliardów'), + 24 => array('kwadrylion','kwadryliony','kwadrylionów'), + 27 => array('kwadryliard','kwadryliardy','kwadryliardów'), + 30 => array('kwintylion','kwintyliony','kwintylionów'), + 33 => array('kwintyliiard','kwintyliardy','kwintyliardów'), + 36 => array('sekstylion','sekstyliony','sekstylionów'), + 39 => array('sekstyliard','sekstyliardy','sekstyliardów'), + 42 => array('septylion','septyliony','septylionów'), + 45 => array('septyliard','septyliardy','septyliardów'), + 48 => array('oktylion','oktyliony','oktylionów'), + 51 => array('oktyliard','oktyliardy','oktyliardów'), + 54 => array('nonylion','nonyliony','nonylionów'), + 57 => array('nonyliard','nonyliardy','nonyliardów'), + 60 => array('decylion','decyliony','decylionów'), + 63 => array('decyliard','decyliardy','decyliardów'), + 100 => array('centylion','centyliony','centylionów'), + 103 => array('centyliard','centyliardy','centyliardów'), + 120 => array('wicylion','wicylion','wicylion'), + 123 => array('wicyliard','wicyliardy','wicyliardów'), + 180 => array('trycylion','trycylion','trycylion'), + 183 => array('trycyliard','trycyliardy','trycyliardów'), + 240 => array('kwadragilion','kwadragilion','kwadragilion'), + 243 => array('kwadragiliard','kwadragiliardy','kwadragiliardów'), + 300 => array('kwinkwagilion','kwinkwagilion','kwinkwagilion'), + 303 => array('kwinkwagiliard','kwinkwagiliardy','kwinkwagiliardów'), + 360 => array('seskwilion','seskwilion','seskwilion'), + 363 => array('seskwiliard','seskwiliardy','seskwiliardów'), + 420 => array('septagilion','septagilion','septagilion'), + 423 => array('septagiliard','septagiliardy','septagiliardów'), + 480 => array('oktogilion','oktogilion','oktogilion'), + 483 => array('oktogiliard','oktogiliardy','oktogiliardów'), + 540 => array('nonagilion','nonagilion','nonagilion'), + 543 => array('nonagiliard','nonagiliardy','nonagiliardów'), + 600 => array('centylion','centyliony','centylionów'), + 603 => array('centyliard','centyliardy','centyliardów'), + 6000018 => array('milinilitrylion','milinilitryliony','milinilitrylionów') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'zero', 'jeden', 'dwa', 'trzy', 'cztery', + 'piêæ', 'sze¶æ', 'siedem', 'osiem', 'dziewiêæ' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://www.xe.com/iso4217.htm Currency codes + * @link http://www.republika.pl/geographia/peuropy.htm Europe review + * @link http://pieniadz.hoga.pl/waluty_objasnienia.asp Currency service + * @access private + */ + var $_currency_names = array( + 'ALL' => array(array('lek','leki','leków'), array('quindarka','quindarki','quindarek')), + 'AUD' => array(array('dolar australijski', 'dolary australijskie', 'dolarów australijskich'), array('cent', 'centy', 'centów')), + 'BAM' => array(array('marka','marki','marek'), array('fenig','fenigi','fenigów')), + 'BGN' => array(array('lew','lewy','lew'), array('stotinka','stotinki','stotinek')), + 'BRL' => array(array('real','reale','realów'), array('centavos','centavos','centavos')), + 'BYR' => array(array('rubel','ruble','rubli'), array('kopiejka','kopiejki','kopiejek')), + 'CAD' => array(array('dolar kanadyjski', 'dolary kanadyjskie', 'dolarów kanadyjskich'), array('cent', 'centy', 'centów')), + 'CHF' => array(array('frank szwajcarski','franki szwajcarskie','franków szwajcarskich'), array('rapp','rappy','rappów')), + 'CYP' => array(array('funt cypryjski','funty cypryjskie','funtów cypryjskich'), array('cent', 'centy', 'centów')), + 'CZK' => array(array('korona czeska','korony czeskie','koron czeskich'), array('halerz','halerze','halerzy')), + 'DKK' => array(array('korona duñska','korony duñskie','koron duñskich'), array('ore','ore','ore')), + 'EEK' => array(array('korona estoñska','korony estoñskie','koron estoñskich'), array('senti','senti','senti')), + 'EUR' => array(array('euro', 'euro', 'euro'), array('eurocent', 'eurocenty', 'eurocentów')), + 'GBP' => array(array('funt szterling','funty szterlingi','funtów szterlingów'), array('pens','pensy','pensów')), + 'HKD' => array(array('dolar Hongkongu','dolary Hongkongu','dolarów Hongkongu'), array('cent', 'centy', 'centów')), + 'HRK' => array(array('kuna','kuny','kun'), array('lipa','lipy','lip')), + 'HUF' => array(array('forint','forinty','forintów'), array('filler','fillery','fillerów')), + 'ILS' => array(array('nowy szekel','nowe szekele','nowych szekeli'), array('agora','agory','agorot')), + 'ISK' => array(array('korona islandzka','korony islandzkie','koron islandzkich'), array('aurar','aurar','aurar')), + 'JPY' => array(array('jen','jeny','jenów'), array('sen','seny','senów')), + 'LTL' => array(array('lit','lity','litów'), array('cent', 'centy', 'centów')), + 'LVL' => array(array('³at','³aty','³atów'), array('sentim','sentimy','sentimów')), + 'MKD' => array(array('denar','denary','denarów'), array('deni','deni','deni')), + 'MTL' => array(array('lira maltañska','liry maltañskie','lir maltañskich'), array('centym','centymy','centymów')), + 'NOK' => array(array('korona norweska','korony norweskie','koron norweskich'), array('oere','oere','oere')), + 'PLN' => array(array('z³oty', 'z³ote', 'z³otych'), array('grosz', 'grosze', 'groszy')), + 'ROL' => array(array('lej','leje','lei'), array('bani','bani','bani')), + 'RUB' => array(array('rubel','ruble','rubli'), array('kopiejka','kopiejki','kopiejek')), + 'SEK' => array(array('korona szwedzka','korony szwedzkie','koron szweckich'), array('oere','oere','oere')), + 'SIT' => array(array('tolar','tolary','tolarów'), array('stotinia','stotinie','stotini')), + 'SKK' => array(array('korona s³owacka','korony s³owackie','koron s³owackich'), array('halerz','halerze','halerzy')), + 'TRL' => array(array('lira turecka','liry tureckie','lir tureckich'), array('kurusza','kurysze','kuruszy')), + 'UAH' => array(array('hrywna','hrywna','hrywna'), array('cent', 'centy', 'centów')), + 'USD' => array(array('dolar','dolary','dolarów'), array('cent', 'centy', 'centów')), + 'YUM' => array(array('dinar','dinary','dinarów'), array('para','para','para')), + 'ZAR' => array(array('rand','randy','randów'), array('cent', 'centy', 'centów')) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'PLN'; // Polish zloty + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Polish language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access public + * @author Piotr Klaban + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + switch ($h) { + case 9: + $ret .= $this->_sep . 'dziewiêæset'; + break; + + case 8: + $ret .= $this->_sep . 'osiemset'; + break; + + case 7: + $ret .= $this->_sep . 'siedemset'; + break; + + case 6: + $ret .= $this->_sep . 'sze¶æset'; + break; + + case 5: + $ret .= $this->_sep . 'piêæset'; + break; + + case 4: + $ret .= $this->_sep . 'czterysta'; + break; + + case 3: + $ret .= $this->_sep . 'trzysta'; + break; + + case 2: + $ret .= $this->_sep . 'dwie¶cie'; + break; + + case 1: + $ret .= $this->_sep . 'sto'; + break; + } + + switch ($t) { + case 9: + case 8: + case 7: + case 6: + case 5: + $ret .= $this->_sep . $this->_digits[$t] . 'dziesi±t'; + break; + + case 4: + $ret .= $this->_sep . 'czterdzie¶ci'; + break; + + case 3: + $ret .= $this->_sep . 'trzydzie¶ci'; + break; + + case 2: + $ret .= $this->_sep . 'dwadzie¶cia'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'dziesiêæ'; + break; + + case 1: + $ret .= $this->_sep . 'jedena¶cie'; + break; + + case 2: + case 3: + case 7: + case 8: + $ret .= $this->_sep . $this->_digits[$d] . 'na¶cie'; + break; + + case 4: + $ret .= $this->_sep . 'czterna¶cie'; + break; + + case 5: + $ret .= $this->_sep . 'piêtna¶cie'; + break; + + case 6: + $ret .= $this->_sep . 'szesna¶cie'; + break; + + case 9: + $ret .= $this->_sep . 'dziewiêtna¶cie'; + break; + } + break; + } + + if ($t != 1 && $d > 0) + $ret .= $this->_sep . $this->_digits[$d]; + + if ($t == 1) + $d = 0; + + if (( $h + $t ) > 0 && $d == 1) + $d = 0; + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + switch ($d) { + case 1: + $suf = $lev[0]; + break; + case 2: + case 3: + case 4: + $suf = $lev[1]; + break; + case 0: + case 5: + case 6: + case 7: + case 8: + case 9: + $suf = $lev[2]; + break; + } + $ret .= $this->_sep . $suf; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in Polish language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Piotr Klaban + * @since Numbers_Words 0.4 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = $this->_get_numlevel($decimal); + $ret .= $this->_sep . $curr_names[0][$lev]; + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = $this->_get_numlevel($fraction); + $ret .= $this->_sep . $curr_names[1][$lev]; + } + return $ret; + } + // }}} + // {{{ _get_numlevel() + + /** + * Returns grammatical "level" of the number - this is necessary + * for choosing the right suffix for exponents and currency names. + * + * @param integer $num An integer between -infinity and infinity inclusive + * that need to be converted to words + * + * @return integer The grammatical "level" of the number. + * + * @access private + * @author Piotr Klaban + * @since Numbers_Words 0.4 + */ + function _get_numlevel($num) { + $num = (int)substr($num,-3); + $h = $t = $d = $lev = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return $lev; + break; + } + if ($t == 1) + $d = 0; + + if (( $h + $t ) > 0 && $d == 1) + $d = 0; + + switch ($d) { + case 1: + $lev = 0; + break; + case 2: + case 3: + case 4: + $lev = 1; + break; + default: + $lev = 2; + } + return $lev; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.pt_BR.php b/Numbers/Words/lang.pt_BR.php new file mode 100644 index 0000000..d699dcc --- /dev/null +++ b/Numbers/Words/lang.pt_BR.php @@ -0,0 +1,331 @@ +, Mario H.C.T. +// +----------------------------------------------------------------------+ +// +// $Id: lang.pt_BR.php,v 1.4 2005/09/18 19:52:22 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Brazilian Portuguese language. +// + + +/** + * Class for translating numbers into Brazilian Portuguese. + * + * @author Marcelo Subtil Marcal + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once "Numbers/Words.php"; + +/** + * Class for translating numbers into Brazilian Portuguese. + * + * @author Marcelo Subtil Marcal + * @package Numbers_Words + */ +class Numbers_Words_pt_BR extends Numbers_Words +{ + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'pt_BR'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Brazilian Portuguese'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Português Brasileiro'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'menos'; + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_unidade = array( + '', + 'um', + 'dois', + 'três', + 'quatro', + 'cinco', + 'seis', + 'sete', + 'oito', + 'nove' + ); + + /** + * The array containing numbers 10-19. + * @var array + * @access private + */ + var $_dezena10 = array( + 'dez', + 'onze', + 'doze', + 'treze', + 'quatorze', + 'quinze', + 'dezesseis', + 'dezessete', + 'dezoito', + 'dezenove' + ); + + /** + * The array containing numbers for 10,20,...,90. + * @var array + * @access private + */ + var $_dezena = array( + '', + 'dez', + 'vinte', + 'trinta', + 'quarenta', + 'cinquenta', + 'sessenta', + 'setenta', + 'oitenta', + 'noventa' + ); + + /** + * The array containing numbers for hundrets. + * @var array + * @access private + */ + var $_centena = array( + '', + 'cem', + 'duzentos', + 'trezentos', + 'quatrocentos', + 'quinhentos', + 'seiscentos', + 'setecentos', + 'oitocentos', + 'novecentos' + ); + + /** + * The sufixes for exponents (singular and plural) + * @var array + * @access private + */ + var $_expoente = array( + '', + 'mil', + 'milhão', + 'bilhão', + 'trilhão', + 'quatrilhão', + 'quintilhão', + 'sextilhão', + 'setilhão', + 'octilhão', + 'nonilhão', + 'decilhão', + 'undecilhão', + 'dodecilhão', + 'tredecilhão', + 'quatuordecilhão', + 'quindecilhão', + 'sedecilhão', + 'septendecilhão' + ); + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://30-03-67.dreamstation.com/currency_alfa.htm World Currency Information + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.shoestring.co.kr/world/p.visa/change.htm Currency names in English + * @access private + */ + var $_currency_names = array( + 'BRL' => array(array('rea'), array('centavo')) ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'BRL'; // Real + + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Brazilian Portuguese language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * + * @return string The corresponding word representation + * + * @access public + * @author Marcelo Subtil Marcal + * @since PHP 4.2.3 + */ + function toWords($num) { + + $ret = ''; + + $num = trim($num); + + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + while (strlen($num) % 3 != 0) { + $num = "0" . $num; + } + + $num = ereg_replace("(...)", "\\1.", $num); + $num = ereg_replace("\.$", "", $num); + + $inteiro = explode(".", $num); + + for ($i = 0; $i < count($inteiro); $i++) { + $ret .= (($inteiro[$i] > 100) && ($inteiro[$i] < 200)) ? "cento" : $this->_centena[$inteiro[$i][0]]; + $ret .= ($inteiro[$i][0] && ($inteiro[$i][1] || $inteiro[$i][2])) ? " e " : ""; + $ret .= ($inteiro[$i][1] < 2) ? "" : $this->_dezena[$inteiro[$i][1]]; + $ret .= (($inteiro[$i][1] > 1) && ($inteiro[$i][2])) ? " e " : ""; + $ret .= ($inteiro > 0) ? ( ($inteiro[$i][1] == 1) ? $this->_dezena10[$inteiro[$i][2]] : $this->_unidade[$inteiro[$i][2]] ) : ""; + $ret .= $inteiro[$i] > 0 ? " " . ($inteiro[$i] > 1 ? str_replace("ão", "ões", $this->_expoente[count($inteiro)-1-$i]) : $this->_expoente[count($inteiro)-1-$i]) : ""; + + if ($ret && (isset($inteiro[$i+1]))) { + if ($inteiro[$i+1] != "000") { + $ret .= ($i+1) == (count($inteiro)-1) ? " e " : ", "; + } + } + + } + + return $ret ? " $ret" : " zero"; + + } + + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in Portuguese language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Mario H.C.T. + * @since Numbers_Words 0.10.1 + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_name[$int_curr])){ + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->toWords($decimal)); + $lev = ($decimal == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[0]) > 1) { + $ret .= $this->_sep . $curr_names[0][$lev]; + } else { + if ($int_curr == "BRL") + $ret .= $this->_sep . $curr_names[0][0] . 'is'; + else + $ret .= $this->_sep . $curr_names[0][0] . 's'; + } + } else { + if ($int_curr == "BRL") + $ret .= $this->_sep . $curr_names[0][0] . 'l'; + else + $ret .= $this->_sep . $curr_names[0][0]; + } + + if ($fraction !== false) { + if ($int_curr == "BRL") + $ret .= $this->_sep . 'e'; + + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->toWords($fraction)); + } else { + $ret .= $this->_sep . $fraction; + } + $lev = ($fraction == 1) ? 0 : 1; + if ($lev > 0) { + if (count($curr_names[1]) > 1) { + $ret .= $this->_sep . $curr_names[1][$lev]; + } else { + $ret .= $this->_sep . $curr_names[1][0] . 's'; + } + } else { + $ret .= $this->_sep . $curr_names[1][0]; + } + } + + return $ret; + } + // }}} +} + +?> diff --git a/Numbers/Words/lang.ru.php b/Numbers/Words/lang.ru.php new file mode 100644 index 0000000..fbf1d35 --- /dev/null +++ b/Numbers/Words/lang.ru.php @@ -0,0 +1,624 @@ + | +// | Andrey Demenev | +// +----------------------------------------------------------------------+ +// +// $Id: lang.ru.php,v 1.3 2006/06/12 13:50:07 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Russian language. +// + +/** + * Class for translating numbers into Russian. + * + * @author Andrey Demenev + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Russian. + * + * @author Andrey Demenev + * @package Numbers_Words + */ +class Numbers_Words_ru extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'ru'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Russian'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Ðóññêèé'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'ìèíóñ'; // minus sign + + /** + * The sufixes for exponents (singular) + * Names partly based on: + * http://home.earthlink.net/~mrob/pub/math/largenum.html + * http://mathforum.org/dr.math/faq/faq.large.numbers.html + * http://www.mazes.com/AmericanNumberingSystem.html + * @var array + * @access private + */ + var $_exponent = array( + 0 => '', + 6 => 'ìèëëèîí', + 9 => 'ìèëëèàðä', + 12 => 'òðèëëèîí', + 15 => 'êâàäðèëëèîí', + 18 => 'êâèíòèëëèîí', + 21 => 'ñåêñòèëëèîí', + 24 => 'ñåïòèëëèîí', + 27 => 'îêòèëëèîí', + 30 => 'íîíèëëèîí', + 33 => 'äåöèëëèîí', + 36 => 'óíäåöèëëèîí', + 39 => 'äóîäåöèëëèîí', + 42 => 'òðåäåöèëëèîí', + 45 => 'êâàòóîðäåöèëëèîí', + 48 => 'êâèíäåöèëëèîí', + 51 => 'ñåêñäåöèëëèîí', + 54 => 'ñåïòåíäåöèëëèîí', + 57 => 'îêòîäåöèëëèîí', + 60 => 'íîâåìäåöèëëèîí', + 63 => 'âèãèíòèëëèîí', + 66 => 'óíâèãèíòèëëèîí', + 69 => 'äóîâèãèíòèëëèîí', + 72 => 'òðåâèãèíòèëëèîí', + 75 => 'êâàòóîðâèãèíòèëëèîí', + 78 => 'êâèíâèãèíòèëëèîí', + 81 => 'ñåêñâèãèíòèëëèîí', + 84 => 'ñåïòåíâèãèíòèëëèîí', + 87 => 'îêòîâèãèíòèëëèîí', + 90 => 'íîâåìâèãèíòèëëèîí', + 93 => 'òðèãèíòèëëèîí', + 96 => 'óíòðèãèíòèëëèîí', + 99 => 'äóîòðèãèíòèëëèîí', + 102 => 'òðåòðèãèíòèëëèîí', + 105 => 'êâàòîðòðèãèíòèëëèîí', + 108 => 'êâèíòðèãèíòèëëèîí', + 111 => 'ñåêñòðèãèíòèëëèîí', + 114 => 'ñåïòåíòðèãèíòèëëèîí', + 117 => 'îêòîòðèãèíòèëëèîí', + 120 => 'íîâåìòðèãèíòèëëèîí', + 123 => 'êâàäðàãèíòèëëèîí', + 126 => 'óíêâàäðàãèíòèëëèîí', + 129 => 'äóîêâàäðàãèíòèëëèîí', + 132 => 'òðåêâàäðàãèíòèëëèîí', + 135 => 'êâàòîðêâàäðàãèíòèëëèîí', + 138 => 'êâèíêâàäðàãèíòèëëèîí', + 141 => 'ñåêñêâàäðàãèíòèëëèîí', + 144 => 'ñåïòåíêâàäðàãèíòèëëèîí', + 147 => 'îêòîêâàäðàãèíòèëëèîí', + 150 => 'íîâåìêâàäðàãèíòèëëèîí', + 153 => 'êâèíêâàãèíòèëëèîí', + 156 => 'óíêâèíêàãèíòèëëèîí', + 159 => 'äóîêâèíêàãèíòèëëèîí', + 162 => 'òðåêâèíêàãèíòèëëèîí', + 165 => 'êâàòîðêâèíêàãèíòèëëèîí', + 168 => 'êâèíêâèíêàãèíòèëëèîí', + 171 => 'ñåêñêâèíêàãèíòèëëèîí', + 174 => 'ñåïòåíêâèíêàãèíòèëëèîí', + 177 => 'îêòîêâèíêàãèíòèëëèîí', + 180 => 'íîâåìêâèíêàãèíòèëëèîí', + 183 => 'ñåêñàãèíòèëëèîí', + 186 => 'óíñåêñàãèíòèëëèîí', + 189 => 'äóîñåêñàãèíòèëëèîí', + 192 => 'òðåñåêñàãèíòèëëèîí', + 195 => 'êâàòîðñåêñàãèíòèëëèîí', + 198 => 'êâèíñåêñàãèíòèëëèîí', + 201 => 'ñåêññåêñàãèíòèëëèîí', + 204 => 'ñåïòåíñåêñàãèíòèëëèîí', + 207 => 'îêòîñåêñàãèíòèëëèîí', + 210 => 'íîâåìñåêñàãèíòèëëèîí', + 213 => 'ñåïòàãèíòèëëèîí', + 216 => 'óíñåïòàãèíòèëëèîí', + 219 => 'äóîñåïòàãèíòèëëèîí', + 222 => 'òðåñåïòàãèíòèëëèîí', + 225 => 'êâàòîðñåïòàãèíòèëëèîí', + 228 => 'êâèíñåïòàãèíòèëëèîí', + 231 => 'ñåêññåïòàãèíòèëëèîí', + 234 => 'ñåïòåíñåïòàãèíòèëëèîí', + 237 => 'îêòîñåïòàãèíòèëëèîí', + 240 => 'íîâåìñåïòàãèíòèëëèîí', + 243 => 'îêòîãèíòèëëèîí', + 246 => 'óíîêòîãèíòèëëèîí', + 249 => 'äóîîêòîãèíòèëëèîí', + 252 => 'òðåîêòîãèíòèëëèîí', + 255 => 'êâàòîðîêòîãèíòèëëèîí', + 258 => 'êâèíîêòîãèíòèëëèîí', + 261 => 'ñåêñîêòîãèíòèëëèîí', + 264 => 'ñåïòîêòîãèíòèëëèîí', + 267 => 'îêòîîêòîãèíòèëëèîí', + 270 => 'íîâåìîêòîãèíòèëëèîí', + 273 => 'íîíàãèíòèëëèîí', + 276 => 'óííîíàãèíòèëëèîí', + 279 => 'äóîíîíàãèíòèëëèîí', + 282 => 'òðåíîíàãèíòèëëèîí', + 285 => 'êâàòîðíîíàãèíòèëëèîí', + 288 => 'êâèííîíàãèíòèëëèîí', + 291 => 'ñåêñíîíàãèíòèëëèîí', + 294 => 'ñåïòåííîíàãèíòèëëèîí', + 297 => 'îêòîíîíàãèíòèëëèîí', + 300 => 'íîâåìíîíàãèíòèëëèîí', + 303 => 'öåíòèëëèîí' + ); + + /** + * The array containing the teens' :) names + * @var array + * @access private + */ + var $_teens = array( + 11=>'îäèííàäöàòü', + 12=>'äâåíàäöàòü', + 13=>'òðèíàäöàòü', + 14=>'÷åòûðíàäöàòü', + 15=>'ïÿòíàäöàòü', + 16=>'øåñòíàäöàòü', + 17=>'ñåìíàäöàòü', + 18=>'âîñåìíàäöàòü', + 19=>'äåâÿòíàäöàòü' + ); + + /** + * The array containing the tens' names + * @var array + * @access private + */ + var $_tens = array( + 2=>'äâàäöàòü', + 3=>'òðèäöàòü', + 4=>'ñîðîê', + 5=>'ïÿòüäåñÿò', + 6=>'øåñòüäåñÿò', + 7=>'ñåìüäåñÿò', + 8=>'âîñåìüäåñÿò', + 9=>'äåâÿíîñòî' + ); + + /** + * The array containing the hundreds' names + * @var array + * @access private + */ + var $_hundreds = array( + 1=>'ñòî', + 2=>'äâåñòè', + 3=>'òðèñòà', + 4=>'÷åòûðåñòà', + 5=>'ïÿòüñîò', + 6=>'øåñòüñîò', + 7=>'ñåìüñîò', + 8=>'âîñåìüñîò', + 9=>'äåâÿòüñîò' + ); + + /** + * The array containing the digits + * for neutral, male and female + * @var array + * @access private + */ + var $_digits = array( + array('íîëü', 'îäíî', 'äâà', 'òðè', '÷åòûðå','ïÿòü', 'øåñòü', 'ñåìü', 'âîñåìü', 'äåâÿòü'), + array('íîëü', 'îäèí', 'äâà', 'òðè', '÷åòûðå','ïÿòü', 'øåñòü', 'ñåìü', 'âîñåìü', 'äåâÿòü'), + array('íîëü', 'îäíà', 'äâå', 'òðè', '÷åòûðå','ïÿòü', 'øåñòü', 'ñåìü', 'âîñåìü', 'äåâÿòü') + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ' '; + + /** + * The currency names (based on the below links, + * informations from central bank websites and on encyclopedias) + * + * @var array + * @link http://www.jhall.demon.co.uk/currency/by_abbrev.html World currencies + * @link http://www.rusimpex.ru/Content/Reference/Refinfo/valuta.htm Foreign currencies names + * @link http://www.cofe.ru/Finance/money.asp Currencies names + * @access private + */ + var $_currency_names = array( + 'ALL' => array( + array(1,'ëåê','ëåêà','ëåêîâ'), + array(2,'êèíäàðêà','êèíäàðêè','êèíäàðîê') + ), + 'AUD' => array( + array(1,'àâñòðàëèéñêèé äîëëàð','àâñòðàëèéñêèõ äîëëàðà','àâñòðàëèéñêèõ äîëëàðîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'BGN' => array( + array(1,'ëåâ','ëåâà','ëåâîâ'), + array(2,'ñòîòèíêà','ñòîòèíêè','ñòîòèíîê') + ), + 'BRL' => array( + array(1,'áðàçèëüñêèé ðåàë','áðàçèëüñêèõ ðåàëà','áðàçèëüñêèõ ðåàëîâ'), + array(1,'ñåíòàâî','ñåíòàâî','ñåíòàâî') + ), + 'BYR' => array( + array(1,'áåëîðóññêèé ðóáëü','áåëîðóññêèõ ðóáëÿ','áåëîðóññêèõ ðóáëåé'), + array(2,'êîïåéêà','êîïåéêè','êîïååê') + ), + 'CAD' => array( + array(1,'êàíàäñêèé äîëëàð','êàíàäñêèõ äîëëàðà','êàíàäñêèõ äîëëàðîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'CHF' => array( + array(1,'øâåéöàðñêèé ôðàíê','øâåéöàðñêèõ ôðàíêà','øâåéöàðñêèõ ôðàíêîâ'), + array(1,'ñàíòèì','ñàíòèìà','ñàíòèìîâ') + ), + 'CYP' => array( + array(1,'êèïðñêèé ôóíò','êèïðñêèõ ôóíòà','êèïðñêèõ ôóíòîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'CZK' => array( + array(2,'÷åøñêàÿ êðîíà','÷åøñêèõ êðîíû','÷åøñêèõ êðîí'), + array(1,'ãàëèðæ','ãàëèðæà','ãàëèðæåé') + ), + 'DKK' => array( + array(2,'äàòñêàÿ êðîíà','äàòñêèõ êðîíû','äàòñêèõ êðîí'), + array(1,'ýðå','ýðå','ýðå') + ), + 'EEK' => array( + array(2,'ýñòîíñêàÿ êðîíà','ýñòîíñêèõ êðîíû','ýñòîíñêèõ êðîí'), + array(1,'ñåíòè','ñåíòè','ñåíòè') + ), + 'EUR' => array( + array(1,'åâðî','åâðî','åâðî'), + array(1,'åâðîöåíò','åâðîöåíòà','åâðîöåíòîâ') + ), + 'CYP' => array( + array(1,'ôóíò ñòåðëèíãîâ','ôóíòà ñòåðëèíãîâ','ôóíòîâ ñòåðëèíãîâ'), + array(1,'ïåíñ','ïåíñà','ïåíñîâ') + ), + 'CAD' => array( + array(1,'ãîíêîíãñêèé äîëëàð','ãîíêîíãñêèõ äîëëàðà','ãîíêîíãñêèõ äîëëàðîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'HRK' => array( + array(2,'õîðâàòñêàÿ êóíà','õîðâàòñêèõ êóíû','õîðâàòñêèõ êóí'), + array(2,'ëèïà','ëèïû','ëèï') + ), + 'HUF' => array( + array(1,'âåíãåðñêèé ôîðèíò','âåíãåðñêèõ ôîðèíòà','âåíãåðñêèõ ôîðèíòîâ'), + array(1,'ôèëëåð','ôèëëåðà','ôèëëåðîâ') + ), + 'ISK' => array( + array(2,'èñëàíäñêàÿ êðîíà','èñëàíäñêèõ êðîíû','èñëàíäñêèõ êðîí'), + array(1,'ýðå','ýðå','ýðå') + ), + 'JPY' => array( + array(2,'èåíà','èåíû','èåí'), + array(2,'ñåíà','ñåíû','ñåí') + ), + 'LTL' => array( + array(1,'ëèò','ëèòà','ëèòîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'LVL' => array( + array(1,'ëàò','ëàòà','ëàòîâ'), + array(1,'ñåíòèì','ñåíòèìà','ñåíòèìîâ') + ), + 'MKD' => array( + array(1,'ìàêåäîíñêèé äèíàð','ìàêåäîíñêèõ äèíàðà','ìàêåäîíñêèõ äèíàðîâ'), + array(1,'äåíè','äåíè','äåíè') + ), + 'MTL' => array( + array(2,'ìàëüòèéñêàÿ ëèðà','ìàëüòèéñêèõ ëèðû','ìàëüòèéñêèõ ëèð'), + array(1,'ñåíòèì','ñåíòèìà','ñåíòèìîâ') + ), + 'NOK' => array( + array(2,'íîðâåæñêàÿ êðîíà','íîðâåæñêèõ êðîíû','íîðâåæñêèõ êðîí'), + array(0,'ýðå','ýðå','ýðå') + ), + 'PLN' => array( + array(1,'çëîòûé','çëîòûõ','çëîòûõ'), + array(1,'ãðîø','ãðîøà','ãðîøåé') + ), + 'ROL' => array( + array(1,'ðóìûíñêèé ëåé','ðóìûíñêèõ ëåé','ðóìûíñêèõ ëåé'), + array(1,'áàíè','áàíè','áàíè') + ), + // both RUR and RUR are used, I use RUB for shorter form + 'RUB' => array( + array(1,'ðóáëü','ðóáëÿ','ðóáëåé'), + array(2,'êîïåéêà','êîïåéêè','êîïååê') + ), + 'RUR' => array( + array(1,'ðîññèéñêèé ðóáëü','ðîññèéñêèõ ðóáëÿ','ðîññèéñêèõ ðóáëåé'), + array(2,'êîïåéêà','êîïåéêè','êîïååê') + ), + 'SEK' => array( + array(2,'øâåäñêàÿ êðîíà','øâåäñêèõ êðîíû','øâåäñêèõ êðîí'), + array(1,'ýðå','ýðå','ýðå') + ), + 'SIT' => array( + array(1,'ñëîâåíñêèé òîëàð','ñëîâåíñêèõ òîëàðà','ñëîâåíñêèõ òîëàðîâ'), + array(2,'ñòîòèíà','ñòîòèíû','ñòîòèí') + ), + 'SKK' => array( + array(2,'ñëîâàöêàÿ êðîíà','ñëîâàöêèõ êðîíû','ñëîâàöêèõ êðîí'), + array(0,'','','') + ), + 'TRL' => array( + array(2,'òóðåöêàÿ ëèðà','òóðåöêèõ ëèðû','òóðåöêèõ ëèð'), + array(1,'ïèàñòð','ïèàñòðà','ïèàñòðîâ') + ), + 'UAH' => array( + array(2,'ãðèâíà','ãðèâíû','ãðèâåí'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'USD' => array( + array(1,'äîëëàð ÑØÀ','äîëëàðà ÑØÀ','äîëëàðîâ ÑØÀ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ), + 'YUM' => array( + array(1,'þãîñëàâñêèé äèíàð','þãîñëàâñêèõ äèíàðà','þãîñëàâñêèõ äèíàðîâ'), + array(1,'ïàðà','ïàðà','ïàðà') + ), + 'ZAR' => array( + array(1,'ðàíä','ðàíäà','ðàíäîâ'), + array(1,'öåíò','öåíòà','öåíòîâ') + ) + ); + + /** + * The default currency name + * @var string + * @access public + */ + var $def_currency = 'RUB'; // Russian rouble + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Russian language + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $gender Gender of string, 0=neutral, 1=male, 2=female. + * Optional, defaults to 1. + * + * @return string The corresponding word representation + * + * @access private + * @author Andrey Demenev + */ + function toWords($num, $gender = 1) + { + return $this->_toWordsWithCase($num, $dummy, $gender); + } + + /** + * Converts a number to its word representation + * in Russian language and determines the case of string. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $case A variable passed by reference which is set to case + * of the word associated with the number + * @param integer $gender Gender of string, 0=neutral, 1=male, 2=female. + * Optional, defaults to 1. + * + * @return string The corresponding word representation + * + * @access private + * @author Andrey Demenev + */ + function _toWordsWithCase($num, &$case, $gender = 1) + { + $ret = ''; + $case = 3; + + $num = trim($num); + + $sign = ""; + if (substr($num, 0, 1) == '-') { + $sign = $this->_minus . $this->_sep; + $num = substr($num, 1); + } + + while (strlen($num) % 3) $num = '0' . $num; + if ($num == 0 || $num == '') { + $ret .= $this->_digits[$gender][0]; + } + + else { + $power = 0; + while ($power < strlen($num)) { + if (!$power) { + $groupgender = $gender; + } elseif ($power == 3) { + $groupgender = 2; + } else { + $groupgender = 1; + } + $group = $this->_groupToWords(substr($num,-$power-3,3),$groupgender,$_case); + if (!$power) { + $case = $_case; + } + if ($power == 3) { + if ($_case == 1) { + $group .= $this->_sep . 'òûñÿ÷à'; + } elseif ($_case == 2) { + $group .= $this->_sep . 'òûñÿ÷è'; + } else { + $group .= $this->_sep . 'òûñÿ÷'; + } + } elseif ($group && $power>3 && isset($this->_exponent[$power])) { + $group .= $this->_sep . $this->_exponent[$power]; + if ($_case == 2) { + $group .= 'à'; + } elseif ($_case == 3) { + $group .= 'îâ'; + } + } + if ($group) { + $ret = $group . $this->_sep . $ret; + } + $power+=3; + } + } + + return $sign . $ret; + } + + // }}} + // {{{ _groupToWords() + + /** + * Converts a group of 3 digits to its word representation + * in Russian language. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $gender Gender of string, 0=neutral, 1=male, 2=female. + * @param integer $case A variable passed by reference which is set to case + * of the word associated with the number + * + * @return string The corresponding word representation + * + * @access private + * @author Andrey Demenev + */ + function _groupToWords($num, $gender, &$case) + { + $ret = ''; + $case = 3; + if ((int)$num == 0) { + $ret = ''; + } elseif ($num < 10) { + $ret = $this->_digits[$gender][(int)$num]; + if ($num == 1) $case = 1; + elseif ($num < 5) $case = 2; + else $case = 3; + } else { + $num = str_pad($num,3,'0',STR_PAD_LEFT); + $hundreds = (int)$num{0}; + if ($hundreds) { + $ret = $this->_hundreds[$hundreds]; + if (substr($num,1) != '00') { + $ret .= $this->_sep; + } + $case = 3; + } + $tens=(int)$num{1}; + $ones=(int)$num{2}; + if ($tens || $ones) { + if ($tens == 1 && $ones == 0) $ret .= 'äåñÿòü'; + elseif ($tens == 1) $ret .= $this->_teens[$ones+10]; + else { + if ($tens > 0) { + $ret .= $this->_tens[(int)$tens]; + } + if ($ones > 0) { + $ret .= $this->_sep + .$this->_digits[$gender][$ones]; + if ($ones == 1) { + $case = 1; + } elseif ($ones < 5) { + $case = 2; + } else { + $case = 3; + } + } + } + } + } + return $ret; + } + // }}} + // {{{ toCurrencyWords() + + /** + * Converts a currency value to its word representation + * (with monetary units) in Russian language + * + * @param integer $int_curr An international currency symbol + * as defined by the ISO 4217 standard (three characters) + * @param integer $decimal A money total amount without fraction part (e.g. amount of dollars) + * @param integer $fraction Fractional part of the money amount (e.g. amount of cents) + * Optional. Defaults to false. + * @param integer $convert_fraction Convert fraction to words (left as numeric if set to false). + * Optional. Defaults to true. + * + * @return string The corresponding word representation for the currency + * + * @access public + * @author Andrey Demenev + */ + function toCurrencyWords($int_curr, $decimal, $fraction = false, $convert_fraction = true) + { + $int_curr = strtoupper($int_curr); + if (!isset($this->_currency_names[$int_curr])) { + $int_curr = $this->def_currency; + } + $curr_names = $this->_currency_names[$int_curr]; + $ret = trim($this->_toWordsWithCase($decimal, $case, $curr_names[0][0])); + $ret .= $this->_sep . $curr_names[0][$case]; + + if ($fraction !== false) { + if ($convert_fraction) { + $ret .= $this->_sep . trim($this->_toWordsWithCase($fraction, $case, $curr_names[1][0])); + } else { + $ret .= $this->_sep . $fraction; + } + $ret .= $this->_sep . $curr_names[1][$case]; + } + return $ret; + } + // }}} + +} + +?> diff --git a/Numbers/Words/lang.sv.php b/Numbers/Words/lang.sv.php new file mode 100644 index 0000000..27195f1 --- /dev/null +++ b/Numbers/Words/lang.sv.php @@ -0,0 +1,310 @@ + | +// | Robin Ericsson | +// +----------------------------------------------------------------------+ +// +// $Id: lang.sv.php,v 1.1 2004/09/02 11:07:59 makler Exp $ +// +// Numbers_Words class extension to spell numbers in Swedish language. +// + +/** + * + * Class for translating numbers into Swedish. + * @author Robin Ericsson + * @package Numbers_Words + */ + +/** + * Include needed files + */ +require_once("Numbers/Words.php"); + +/** + * Class for translating numbers into Swedish. + * + * @author Robin Ericsson + * @package Numbers_Words + */ +class Numbers_Words_sv extends Numbers_Words +{ + + // {{{ properties + + /** + * Locale name + * @var string + * @access public + */ + var $locale = 'sv'; + + /** + * Language name in English + * @var string + * @access public + */ + var $lang = 'Swedish'; + + /** + * Native language name + * @var string + * @access public + */ + var $lang_native = 'Svenska'; + + /** + * The word for the minus sign + * @var string + * @access private + */ + var $_minus = 'Minus'; // minus sign + + /** + * The sufixes for exponents (singular and plural) + * @var array + * @access private + */ + var $_exponent = array( + 0 => array(''), + 3 => array('tusen', 'tusen'), + 6 => array('miljon','miljoner'), + 9 => array('miljard','miljarder'), + 12 => array('biljon','biljoner'), + 15 => array('biljard','biljarder'), + 18 => array('triljon','triljoner'), + 21 => array('triljard','triljarder'), + 24 => array('kvadriljon','kvadriljoner'), + 27 => array('kvadriljard','kvadriljarder'), + 30 => array('kvintiljon','kvintiljoner'), + 33 => array('kvintiljard','kvintiljarder'), + 36 => array('sextiljon','sextiljoner'), + 39 => array('sextiljard','sextiljarder'), + 42 => array('septiljon','septiljoner'), + 45 => array('septiljard','septiljarder'), + 48 => array('oktiljon','oktiljoner'), + 51 => array('oktiljard','oktiljarder'), + 54 => array('noniljon','noniljoner'), + 57 => array('noniljard','noniljarder'), + 60 => array('dekiljon','dekiljoner'), + 63 => array('dekiljard','dekiljarder'), + 120 => array('vigintiljon','vigintiljoner'), + 123 => array('vigintiljard','vigintiljarder'), + 600 => array('centiljon','centiljoner'), + 603 => array('centiljard','centiljarder') + ); + + /** + * The array containing the digits (indexed by the digits themselves). + * @var array + * @access private + */ + var $_digits = array( + 0 => 'noll', 'ett', 'två', 'tre', 'fyra', + 'fem', 'sex', 'sju', 'åtta', 'nio' + ); + + /** + * The word separator + * @var string + * @access private + */ + var $_sep = ''; + + /** + * The exponent word separator + * @var string + * @access private + */ + var $_sep2 = ' '; + + // }}} + // {{{ toWords() + + /** + * Converts a number to its word representation + * in Swedish language. + * + * @param integer $num An integer between -infinity and infinity inclusive :) + * that need to be converted to words + * @param integer $power The power of ten for the rest of the number to the right. + * Optional, defaults to 0. + * @param integer $powsuffix The power name to be added to the end of the return string. + * Used internally. Optional, defaults to ''. + * + * @return string The corresponding word representation + * + * @access private + * @author Piotr Klaban + * @author Robin Ericsson + * @since PHP 4.2.3 + */ + function toWords($num, $power = 0, $powsuffix = '') { + $ret = ''; + + // add a minus sign + if (substr($num, 0, 1) == '-') { + $ret = $this->_sep . $this->_minus; + $num = substr($num, 1); + } + + // strip excessive zero signs and spaces + $num = trim($num); + $num = preg_replace('/^0+/','',$num); + + if (strlen($num) > 3) { + $maxp = strlen($num)-1; + $curp = $maxp; + for ($p = $maxp; $p > 0; --$p) { // power + + // check for highest power + if (isset($this->_exponent[$p])) { + // send substr from $curp to $p + $snum = substr($num, $maxp - $curp, $curp - $p + 1); + $snum = preg_replace('/^0+/','',$snum); + if ($snum !== '') { + $cursuffix = $this->_exponent[$power][count($this->_exponent[$power])-1]; + if ($powsuffix != '') + $cursuffix .= $this->_sep . $powsuffix; + $ret .= $this->toWords($snum, $p, $cursuffix); + } + $curp = $p - 1; + continue; + } + } + $num = substr($num, $maxp - $curp, $curp - $p + 1); + if ($num == 0) { + return $ret; + } + } elseif ($num == 0 || $num == '') { + return $this->_sep . $this->_digits[0]; + } + + $h = $t = $d = 0; + + switch(strlen($num)) { + case 3: + $h = (int)substr($num,-3,1); + + case 2: + $t = (int)substr($num,-2,1); + + case 1: + $d = (int)substr($num,-1,1); + break; + + case 0: + return; + break; + } + + if ($h) { + $ret .= $this->_sep . $this->_digits[$h] . $this->_sep . 'hundra'; + } + + // ten, twenty etc. + switch ($t) { + case 5: + case 6: + case 7: + $ret .= $this->_sep . $this->_digits[$t] . 'tio'; + break; + + case 9: + $ret .= $this->_sep . 'nittio'; + break; + + case 8: + $ret .= $this->_sep . 'åttio'; + break; + + case 4: + $ret .= $this->_sep . 'fyrtio'; + break; + + case 3: + $ret .= $this->_sep . 'trettio'; + break; + + case 2: + $ret .= $this->_sep . 'tjugo'; + break; + + case 1: + switch ($d) { + case 0: + $ret .= $this->_sep . 'tio'; + break; + + case 1: + $ret .= $this->_sep . 'elva'; + break; + + case 2: + $ret .= $this->_sep . 'tolv'; + break; + + case 3: + $ret .= $this->_sep . 'tretton'; + break; + + case 4: + $ret .= $this->_sep . 'fjorton'; + break; + + case 5: + case 6: + $ret .= $this->_sep . $this->_digits[$d] . 'ton'; + break; + + case 7: + $ret .= $this->_sep . 'sjutton'; + break; + + case 8: + $ret .= $this->_sep . 'arton'; + break; + case 9: + $ret .= $this->_sep . 'nitton'; + } + break; + } + + if ($t != 1 && $d > 0) { // add digits only in <0>,<1,9> and <21,inf> + // add minus sign between [2-9] and digit + $ret .= $this->_sep . $this->_digits[$d]; + } + + if ($power > 0) { + if (isset($this->_exponent[$power])) + $lev = $this->_exponent[$power]; + + if (!isset($lev) || !is_array($lev)) + return null; + + $ret .= $this->_sep . $lev[0]; + } + + if ($powsuffix != '') + $ret .= $this->_sep . $powsuffix; + + return $ret; + } + // }}} +} + +?> diff --git a/OLE.php b/OLE.php new file mode 100644 index 0000000..637e189 --- /dev/null +++ b/OLE.php @@ -0,0 +1,410 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +// $Id: OLE.php,v 1.7 2003/08/21 15:15:40 xnoguer Exp $ + + +/** +* Constants for OLE package +*/ +define('OLE_PPS_TYPE_ROOT', 5); +define('OLE_PPS_TYPE_DIR', 1); +define('OLE_PPS_TYPE_FILE', 2); +define('OLE_DATA_SIZE_SMALL', 0x1000); +define('OLE_LONG_INT_SIZE', 4); +define('OLE_PPS_SIZE', 0x80); + +require_once('PEAR.php'); +require_once 'OLE/PPS.php'; + +/** +* OLE package base class. +* +* @author Xavier Noguer +* @category Structures +* @package OLE +*/ +class OLE extends PEAR +{ + /** + * The file handle for reading an OLE container + * @var resource + */ + var $_file_handle; + + /** + * Array of PPS's found on the OLE container + * @var array + */ + var $_list; + + /** + * Creates a new OLE object + * Remember to use ampersand when creating an OLE object ($my_ole =& new OLE();) + * @access public + */ + function OLE() + { + $this->_list = array(); + } + + /** + * Reads an OLE container from the contents of the file given. + * + * @acces public + * @param string $file + * @return mixed true on success, PEAR_Error on failure + */ + function read($file) + { + /* consider storing offsets as constants */ + $big_block_size_offset = 30; + $iBdbCnt_offset = 44; + $bd_start_offset = 68; + + $fh = @fopen($file, "r"); + if ($fh == false) { + return $this->raiseError("Can't open file $file"); + } + $this->_file_handle = $fh; + + /* begin reading OLE attributes */ + fseek($fh, 0); + $signature = fread($fh, 8); + if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) { + return $this->raiseError("File doesn't seem to be an OLE container."); + } + fseek($fh, $big_block_size_offset); + $packed_array = unpack("v", fread($fh, 2)); + $big_block_size = pow(2, $packed_array['']); + + $packed_array = unpack("v", fread($fh, 2)); + $small_block_size = pow(2, $packed_array['']); + $i1stBdL = ($big_block_size - 0x4C) / OLE_LONG_INT_SIZE; + + fseek($fh, $iBdbCnt_offset); + $packed_array = unpack("V", fread($fh, 4)); + $iBdbCnt = $packed_array['']; + + $packed_array = unpack("V", fread($fh, 4)); + $pps_wk_start = $packed_array['']; + + fseek($fh, $bd_start_offset); + $packed_array = unpack("V", fread($fh, 4)); + $bd_start = $packed_array['']; + $packed_array = unpack("V", fread($fh, 4)); + $bd_count = $packed_array['']; + $packed_array = unpack("V", fread($fh, 4)); + $iAll = $packed_array['']; // this may be wrong + /* create OLE_PPS objects from */ + $ret = $this->_readPpsWks($pps_wk_start, $big_block_size); + if (PEAR::isError($ret)) { + return $ret; + } + return true; + } + + /** + * Destructor (using PEAR) + * Just closes the file handle on the OLE file. + * + * @access private + */ + function _OLE() + { + fclose($this->_file_handle); + } + + /** + * Gets information about all PPS's on the OLE container from the PPS WK's + * creates an OLE_PPS object for each one. + * + * @access private + * @param integer $pps_wk_start Position inside the OLE file where PPS WK's start + * @param integer $big_block_size Size of big blobks in the OLE file + * @return mixed true on success, PEAR_Error on failure + */ + function _readPpsWks($pps_wk_start, $big_block_size) + { + $pointer = ($pps_wk_start + 1) * $big_block_size; + while (1) + { + fseek($this->_file_handle, $pointer); + $pps_wk = fread($this->_file_handle, OLE_PPS_SIZE); + if (strlen($pps_wk) != OLE_PPS_SIZE) { + break; // Excel likes to add a trailing byte sometimes + //return $this->raiseError("PPS at $pointer seems too short: ".strlen($pps_wk)); + } + $name_length = unpack("c", substr($pps_wk, 64, 2)); // FIXME (2 bytes??) + $name_length = $name_length[''] - 2; + $name = substr($pps_wk, 0, $name_length); + $type = unpack("c", substr($pps_wk, 66, 1)); + if (($type[''] != OLE_PPS_TYPE_ROOT) and + ($type[''] != OLE_PPS_TYPE_DIR) and + ($type[''] != OLE_PPS_TYPE_FILE)) + { + return $this->raiseError("PPS at $pointer has unknown type: {$type['']}"); + } + $prev = unpack("V", substr($pps_wk, 68, 4)); + $next = unpack("V", substr($pps_wk, 72, 4)); + $dir = unpack("V", substr($pps_wk, 76, 4)); + // there is no magic number, it can take different values. + //$magic = unpack("V", strrev(substr($pps_wk, 92, 4))); + $time_1st = substr($pps_wk, 100, 8); + $time_2nd = substr($pps_wk, 108, 8); + $start_block = unpack("V", substr($pps_wk, 116, 4)); + $size = unpack("V", substr($pps_wk, 120, 4)); + // _data member will point to position in file!! + // OLE_PPS object is created with an empty children array!! + $this->_list[] = new OLE_PPS(null, '', $type[''], $prev[''], $next[''], + $dir[''], OLE::OLE2LocalDate($time_1st), + OLE::OLE2LocalDate($time_2nd), + ($start_block[''] + 1) * $big_block_size, array()); + // give it a size + $this->_list[count($this->_list) - 1]->Size = $size['']; + // check if the PPS tree (starting from root) is complete + if ($this->_ppsTreeComplete(0)) { + break; + } + $pointer += OLE_PPS_SIZE; + } + } + + /** + * It checks whether the PPS tree is complete (all PPS's read) + * starting with the given PPS (not necessarily root) + * + * @access private + * @param integer $index The index of the PPS from which we are checking + * @return boolean Whether the PPS tree for the given PPS is complete + */ + function _ppsTreeComplete($index) + { + if ($this->_list[$index]->NextPps != -1) { + if (!isset($this->_list[$this->_list[$index]->NextPps])) { + return false; + } + else { + return $this->_ppsTreeComplete($this->_list[$index]->NextPps); + } + } + if ($this->_list[$index]->DirPps != -1) { + if (!isset($this->_list[$this->_list[$index]->DirPps])) { + return false; + } + else { + return $this->_ppsTreeComplete($this->_list[$index]->DirPps); + } + } + return true; + } + + /** + * Checks whether a PPS is a File PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @access public + * @param integer $index The index for the PPS + * @return bool true if it's a File PPS, false otherwise + */ + function isFile($index) + { + if (isset($this->_list[$index])) { + return ($this->_list[$index]->Type == OLE_PPS_TYPE_FILE); + } + return false; + } + + /** + * Checks whether a PPS is a Root PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @access public + * @param integer $index The index for the PPS. + * @return bool true if it's a Root PPS, false otherwise + */ + function isRoot($index) + { + if (isset($this->_list[$index])) { + return ($this->_list[$index]->Type == OLE_PPS_TYPE_ROOT); + } + return false; + } + + /** + * Gives the total number of PPS's found in the OLE container. + * + * @access public + * @return integer The total number of PPS's found in the OLE container + */ + function ppsTotal() + { + return count($this->_list); + } + + /** + * Gets data from a PPS + * If there is no PPS for the index given, it will return an empty string. + * + * @access public + * @param integer $index The index for the PPS + * @param integer $position The position from which to start reading + * (relative to the PPS) + * @param integer $length The amount of bytes to read (at most) + * @return string The binary string containing the data requested + */ + function getData($index, $position, $length) + { + // if position is not valid return empty string + if (!isset($this->_list[$index]) or ($position >= $this->_list[$index]->Size) or ($position < 0)) { + return ''; + } + // Beware!!! _data member is actually a position + fseek($this->_file_handle, $this->_list[$index]->_data + $position); + return fread($this->_file_handle, $length); + } + + /** + * Gets the data length from a PPS + * If there is no PPS for the index given, it will return 0. + * + * @access public + * @param integer $index The index for the PPS + * @return integer The amount of bytes in data the PPS has + */ + function getDataLength($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Size; + } + return 0; + } + + /** + * Utility function to transform ASCII text to Unicode + * + * @access public + * @static + * @param string $ascii The ASCII string to transform + * @return string The string in Unicode + */ + function Asc2Ucs($ascii) + { + $rawname = ''; + for ($i = 0; $i < strlen($ascii); $i++) { + $rawname .= $ascii{$i}."\x00"; + } + return $rawname; + } + + /** + * Utility function + * Returns a string for the OLE container with the date given + * + * @access public + * @static + * @param integer $date A timestamp + * @return string The string for the OLE container + */ + function LocalDate2OLE($date = null) + { + if (!isset($date)) { + return "\x00\x00\x00\x00\x00\x00\x00\x00"; + } + + // factor used for separating numbers into 4 bytes parts + $factor = pow(2,32); + + // days from 1-1-1601 until the beggining of UNIX era + $days = 134774; + // calculate seconds + $big_date = $days*24*3600 + gmmktime(date("H",$date),date("i",$date),date("s",$date), + date("m",$date),date("d",$date),date("Y",$date)); + // multiply just to make MS happy + $big_date *= 10000000; + + $high_part = floor($big_date/$factor); + // lower 4 bytes + $low_part = floor((($big_date/$factor) - $high_part)*$factor); + + // Make HEX string + $res = ''; + + for ($i=0; $i<4; $i++) + { + $hex = $low_part % 0x100; + $res .= pack('c', $hex); + $low_part /= 0x100; + } + for ($i=0; $i<4; $i++) + { + $hex = $high_part % 0x100; + $res .= pack('c', $hex); + $high_part /= 0x100; + } + return $res; + } + + /** + * Returns a timestamp from an OLE container's date + * + * @access public + * @static + * @param integer $string A binary string with the encoded date + * @return string The timestamp corresponding to the string + */ + function OLE2LocalDate($string) + { + if (strlen($string) != 8) { + return new PEAR_Error("Expecting 8 byte string"); + } + + // factor used for separating numbers into 4 bytes parts + $factor = pow(2,32); + $high_part = 0; + for ($i=0; $i<4; $i++) + { + $al = unpack('C', $string{(7 - $i)}); + $high_part += $al['']; + if ($i < 3) { + $high_part *= 0x100; + } + } + $low_part = 0; + for ($i=4; $i<8; $i++) + { + $al = unpack('C', $string{(7 - $i)}); + $low_part += $al['']; + if ($i < 7) { + $low_part *= 0x100; + } + } + $big_date = ($high_part*$factor) + $low_part; + // translate to seconds + $big_date /= 10000000; + + // days from 1-1-1601 until the beggining of UNIX era + $days = 134774; + + // translate to seconds from beggining of UNIX era + $big_date -= $days*24*3600; + return floor($big_date); + } +} +?> diff --git a/OLE/PPS.php b/OLE/PPS.php new file mode 100644 index 0000000..03dbd90 --- /dev/null +++ b/OLE/PPS.php @@ -0,0 +1,219 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +// $Id: PPS.php,v 1.5 2003/12/14 18:12:28 xnoguer Exp $ + + +require_once('PEAR.php'); +require_once('OLE.php'); + +/** +* Class for creating PPS's for OLE containers +* +* @author Xavier Noguer +* @category Structures +* @package OLE +*/ +class OLE_PPS extends PEAR +{ + /** + * The PPS index + * @var integer + */ + var $No; + + /** + * The PPS name (in Unicode) + * @var string + */ + var $Name; + + /** + * The PPS type. Dir, Root or File + * @var integer + */ + var $Type; + + /** + * The index of the previous PPS + * @var integer + */ + var $PrevPps; + + /** + * The index of the next PPS + * @var integer + */ + var $NextPps; + + /** + * The index of it's first child if this is a Dir or Root PPS + * @var integer + */ + var $DirPps; + + /** + * A timestamp + * @var integer + */ + var $Time1st; + + /** + * A timestamp + * @var integer + */ + var $Time2nd; + + /** + * Starting block (small or big) for this PPS's data inside the container + * @var integer + */ + var $_StartBlock; + + /** + * The size of the PPS's data (in bytes) + * @var integer + */ + var $Size; + + /** + * The PPS's data (only used if it's not using a temporary file) + * @var string + */ + var $_data; + + /** + * Array of child PPS's (only used by Root and Dir PPS's) + * @var array + */ + var $children = array(); + + /** + * The constructor + * + * @access public + * @param integer $No The PPS index + * @param string $name The PPS name (in Unicode) + * @param integer $type The PPS type. Dir, Root or File + * @param integer $prev The index of the previous PPS + * @param integer $next The index of the next PPS + * @param integer $dir The index of it's first child if this is a Dir or Root PPS + * @param integer $time_1st A timestamp + * @param integer $time_2nd A timestamp + * @param array $children Array containing children PPS for this PPS + */ + function OLE_PPS($No, $name, $type, $prev, $next, $dir, $time_1st, $time_2nd, $data, $children) + { + $this->No = $No; + $this->Name = $name; + $this->Type = $type; + $this->PrevPps = $prev; + $this->NextPps = $next; + $this->DirPps = $dir; + $this->Time1st = $time_1st; + $this->Time2nd = $time_2nd; + $this->_data = $data; + $this->children = $children; + if ($data != '') { + $this->Size = strlen($data); + } + else { + $this->Size = 0; + } + } + + /** + * Returns the amount of data saved for this PPS + * + * @access private + * @return integer The amount of data (in bytes) + */ + function _DataLen() + { + if (!isset($this->_data)) { + return 0; + } + if (isset($this->_PPS_FILE)) + { + fseek($this->_PPS_FILE, 0); + $stats = fstat($this->_PPS_FILE); + return $stats[7]; + } + else { + return strlen($this->_data); + } + } + + /** + * Returns a string with the PPS's WK (What is a WK?) + * + * @access private + * @return string The binary string + */ + function _getPpsWk() + { + $ret = $this->Name; + for ($i = 0; $i < (64 - strlen($this->Name)); $i++) { + $ret .= "\x00"; + } + $ret .= pack("v", strlen($this->Name) + 2) // 66 + . pack("c", $this->Type) // 67 + . pack("c", 0x00) //UK // 68 + . pack("V", $this->PrevPps) //Prev // 72 + . pack("V", $this->NextPps) //Next // 76 + . pack("V", $this->DirPps) //Dir // 80 + . "\x00\x09\x02\x00" // 84 + . "\x00\x00\x00\x00" // 88 + . "\xc0\x00\x00\x00" // 92 + . "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root + . "\x00\x00\x00\x00" // 100 + . OLE::LocalDate2OLE($this->Time1st) // 108 + . OLE::LocalDate2OLE($this->Time2nd) // 116 + . pack("V", isset($this->_StartBlock)? + $this->_StartBlock:0) // 120 + . pack("V", $this->Size) // 124 + . pack("V", 0); // 128 + return $ret; + } + + /** + * Updates index and pointers to previous, next and children PPS's for this + * PPS. I don't think it'll work with Dir PPS's. + * + * @access private + * @param array &$pps_array Reference to the array of PPS's for the whole OLE + * container + * @return integer The index for this PPS + */ + function _savePpsSetPnt(&$pps_array) + { + $pps_array[count($pps_array)] = &$this; + $this->No = count($pps_array) - 1; + $this->PrevPps = 0xFFFFFFFF; + $this->NextPps = 0xFFFFFFFF; + if (count($this->children) > 0) { + $this->DirPps = $this->children[0]->_savePpsSetPnt($pps_array); + } + else { + $this->DirPps = 0xFFFFFFFF; + } + return $this->No; + } +} +?> diff --git a/OLE/PPS/File.php b/OLE/PPS/File.php new file mode 100644 index 0000000..852c7af --- /dev/null +++ b/OLE/PPS/File.php @@ -0,0 +1,114 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +// $Id: File.php,v 1.8 2003/12/12 21:10:10 xnoguer Exp $ + + +require_once ('OLE/PPS.php'); + +/** +* Class for creating File PPS's for OLE containers +* +* @author Xavier Noguer +* @category Structures +* @package OLE +*/ +class OLE_PPS_File extends OLE_PPS +{ + /** + * The temporary dir for storing the OLE file + * @var string + */ + var $_tmp_dir; + + /** + * The constructor + * + * @access public + * @param string $name The name of the file (in Unicode) + * @see OLE::Asc2Ucs() + */ + function OLE_PPS_File($name) + { + $this->_tmp_dir = ''; + $this->OLE_PPS( + null, + $name, + OLE_PPS_TYPE_FILE, + null, + null, + null, + null, + null, + '', + array()); + } + + /** + * Sets the temp dir used for storing the OLE file + * + * @access public + * @param string $dir The dir to be used as temp dir + * @return true if given dir is valid, false otherwise + */ + function setTempDir($dir) + { + if (is_dir($dir)) { + $this->_tmp_dir = $dir; + return true; + } + return false; + } + + /** + * Initialization method. Has to be called right after OLE_PPS_File(). + * + * @access public + * @return mixed true on success. PEAR_Error on failure + */ + function init() + { + $this->_tmp_filename = tempnam($this->_tmp_dir, "OLE_PPS_File"); + $fh = @fopen($this->_tmp_filename, "w+b"); + if ($fh == false) { + return $this->raiseError("Can't create temporary file"); + } + $this->_PPS_FILE = $fh; + if ($this->_PPS_FILE) { + fseek($this->_PPS_FILE, 0); + } + } + + /** + * Append data to PPS + * + * @access public + * @param string $data The data to append + */ + function append($data) + { + if ($this->_PPS_FILE) { + fwrite($this->_PPS_FILE, $data); + } + else { + $this->_data .= $data; + } + } +} +?> diff --git a/OLE/PPS/Root.php b/OLE/PPS/Root.php new file mode 100644 index 0000000..9b86d18 --- /dev/null +++ b/OLE/PPS/Root.php @@ -0,0 +1,519 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +// $Id: Root.php,v 1.7 2003/12/12 21:10:10 xnoguer Exp $ + + +require_once ('OLE/PPS.php'); + +/** +* Class for creating Root PPS's for OLE containers +* +* @author Xavier Noguer +* @category Structures +* @package OLE +*/ +class OLE_PPS_Root extends OLE_PPS +{ + /** + * The temporary dir for storing the OLE file + * @var string + */ + var $_tmp_dir; + + /** + * Constructor + * + * @access public + * @param integer $time_1st A timestamp + * @param integer $time_2nd A timestamp + */ + function OLE_PPS_Root($time_1st, $time_2nd, $raChild) + { + $this->_tmp_dir = ''; + $this->OLE_PPS( + null, + OLE::Asc2Ucs('Root Entry'), + OLE_PPS_TYPE_ROOT, + null, + null, + null, + $time_1st, + $time_2nd, + null, + $raChild); + } + + /** + * Sets the temp dir used for storing the OLE file + * + * @access public + * @param string $dir The dir to be used as temp dir + * @return true if given dir is valid, false otherwise + */ + function setTempDir($dir) + { + if (is_dir($dir)) { + $this->_tmp_dir = $dir; + return true; + } + return false; + } + + /** + * Method for saving the whole OLE container (including files). + * In fact, if called with an empty argument (or '-'), it saves to a + * temporary file and then outputs it's contents to stdout. + * + * @param string $filename The name of the file where to save the OLE container + * @access public + * @return mixed true on success, PEAR_Error on failure + */ + function save($filename) + { + // Initial Setting for saving + $this->_BIG_BLOCK_SIZE = pow(2, + ((isset($this->_BIG_BLOCK_SIZE))? $this->_adjust2($this->_BIG_BLOCK_SIZE) : 9)); + $this->_SMALL_BLOCK_SIZE= pow(2, + ((isset($this->_SMALL_BLOCK_SIZE))? $this->_adjust2($this->_SMALL_BLOCK_SIZE): 6)); + + // Open temp file if we are sending output to stdout + if (($filename == '-') or ($filename == '')) + { + $this->_tmp_filename = tempnam($this->_tmp_dir, "OLE_PPS_Root"); + $this->_FILEH_ = @fopen($this->_tmp_filename,"w+b"); + if ($this->_FILEH_ == false) { + return $this->raiseError("Can't create temporary file."); + } + } + else + { + $this->_FILEH_ = @fopen($filename, "wb"); + if ($this->_FILEH_ == false) { + return $this->raiseError("Can't open $filename. It may be in use or protected."); + } + } + // Make an array of PPS's (for Save) + $aList = array(); + $this->_savePpsSetPnt($aList); + // calculate values for header + list($iSBDcnt, $iBBcnt, $iPPScnt) = $this->_calcSize($aList); //, $rhInfo); + // Save Header + $this->_saveHeader($iSBDcnt, $iBBcnt, $iPPScnt); + + // Make Small Data string (write SBD) + $this->_data = $this->_makeSmallData($aList); + + // Write BB + $this->_saveBigData($iSBDcnt, $aList); + // Write PPS + $this->_savePps($aList); + // Write Big Block Depot and BDList and Adding Header informations + $this->_saveBbd($iSBDcnt, $iBBcnt, $iPPScnt); + // Close File, send it to stdout if necessary + if(($filename == '-') or ($filename == '')) + { + fseek($this->_FILEH_, 0); + fpassthru($this->_FILEH_); + @fclose($this->_FILEH_); + // Delete the temporary file. + @unlink($this->_tmp_filename); + } + else { + @fclose($this->_FILEH_); + } + return true; + } + + /** + * Calculate some numbers + * + * @access private + * @param array $raList Reference to an array of PPS's + * @return array The array of numbers + */ + function _calcSize(&$raList) + { + // Calculate Basic Setting + list($iSBDcnt, $iBBcnt, $iPPScnt) = array(0,0,0); + $iSmallLen = 0; + $iSBcnt = 0; + for ($i = 0; $i < count($raList); $i++) { + if($raList[$i]->Type == OLE_PPS_TYPE_FILE) { + $raList[$i]->Size = $raList[$i]->_DataLen(); + if($raList[$i]->Size < OLE_DATA_SIZE_SMALL) { + $iSBcnt += floor($raList[$i]->Size / $this->_SMALL_BLOCK_SIZE) + + (($raList[$i]->Size % $this->_SMALL_BLOCK_SIZE)? 1: 0); + } + else { + $iBBcnt += (floor($raList[$i]->Size / $this->_BIG_BLOCK_SIZE) + + (($raList[$i]->Size % $this->_BIG_BLOCK_SIZE)? 1: 0)); + } + } + } + $iSmallLen = $iSBcnt * $this->_SMALL_BLOCK_SIZE; + $iSlCnt = floor($this->_BIG_BLOCK_SIZE / OLE_LONG_INT_SIZE); + $iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt)? 1:0); + $iBBcnt += (floor($iSmallLen / $this->_BIG_BLOCK_SIZE) + + (( $iSmallLen % $this->_BIG_BLOCK_SIZE)? 1: 0)); + $iCnt = count($raList); + $iBdCnt = $this->_BIG_BLOCK_SIZE / OLE_PPS_SIZE; + $iPPScnt = (floor($iCnt/$iBdCnt) + (($iCnt % $iBdCnt)? 1: 0)); + + return array($iSBDcnt, $iBBcnt, $iPPScnt); + } + + /** + * Helper function for caculating a magic value for block sizes + * + * @access private + * @param integer $i2 The argument + * @see save() + * @return integer + */ + function _adjust2($i2) + { + $iWk = log($i2)/log(2); + return ($iWk > floor($iWk))? floor($iWk)+1:$iWk; + } + + /** + * Save OLE header + * + * @access private + * @param integer $iSBDcnt + * @param integer $iBBcnt + * @param integer $iPPScnt + */ + function _saveHeader($iSBDcnt, $iBBcnt, $iPPScnt) + { + $FILE = $this->_FILEH_; + + // Calculate Basic Setting + $iBlCnt = $this->_BIG_BLOCK_SIZE / OLE_LONG_INT_SIZE; + $i1stBdL = ($this->_BIG_BLOCK_SIZE - 0x4C) / OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBBcnt + $iPPScnt + $iSBDcnt; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt)? 1: 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW+$iBdCntW) % $iBlCnt)? 1: 0); + + // Calculate BD count + if ($iBdCnt >$i1stBdL) + { + while (1) + { + $iBdExL++; + $iAllW++; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt)? 1: 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW+$iBdCntW) % $iBlCnt)? 1: 0); + if ($iBdCnt <= ($iBdExL*$iBlCnt+ $i1stBdL)) { + break; + } + } + } + + // Save Header + fwrite($FILE, + "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack("v", 0x3b) + . pack("v", 0x03) + . pack("v", -2) + . pack("v", 9) + . pack("v", 6) + . pack("v", 0) + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack("V", $iBdCnt) + . pack("V", $iBBcnt+$iSBDcnt) //ROOT START + . pack("V", 0) + . pack("V", 0x1000) + . pack("V", 0) //Small Block Depot + . pack("V", 1) + ); + // Extra BDList Start, Count + if ($iBdCnt < $i1stBdL) + { + fwrite($FILE, + pack("V", -2). // Extra BDList Start + pack("V", 0) // Extra BDList Count + ); + } + else + { + fwrite($FILE, pack("V", $iAll+$iBdCnt) . pack("V", $iBdExL)); + } + + // BDList + for ($i=0; $i<$i1stBdL and $i < $iBdCnt; $i++) { + fwrite($FILE, pack("V", $iAll+$i)); + } + if ($i < $i1stBdL) + { + for ($j = 0; $j < ($i1stBdL-$i); $j++) { + fwrite($FILE, (pack("V", -1))); + } + } + } + + /** + * Saving big data (PPS's with data bigger than OLE_DATA_SIZE_SMALL) + * + * @access private + * @param integer $iStBlk + * @param array &$raList Reference to array of PPS's + */ + function _saveBigData($iStBlk, &$raList) + { + $FILE = $this->_FILEH_; + + // cycle through PPS's + for ($i = 0; $i < count($raList); $i++) + { + if($raList[$i]->Type != OLE_PPS_TYPE_DIR) + { + $raList[$i]->Size = $raList[$i]->_DataLen(); + if(($raList[$i]->Size >= OLE_DATA_SIZE_SMALL) or + (($raList[$i]->Type == OLE_PPS_TYPE_ROOT) and isset($raList[$i]->_data))) + { + // Write Data + if(isset($raList[$i]->_PPS_FILE)) + { + $iLen = 0; + fseek($raList[$i]->_PPS_FILE, 0); // To The Top + while($sBuff = fread($raList[$i]->_PPS_FILE, 4096)) + { + $iLen += strlen($sBuff); + fwrite($FILE, $sBuff); + } + } + else { + fwrite($FILE, $raList[$i]->_data); + } + + if ($raList[$i]->Size % $this->_BIG_BLOCK_SIZE) + { + for ($j = 0; $j < ($this->_BIG_BLOCK_SIZE - ($raList[$i]->Size % $this->_BIG_BLOCK_SIZE)); $j++) { + fwrite($FILE, "\x00"); + } + } + // Set For PPS + $raList[$i]->_StartBlock = $iStBlk; + $iStBlk += + (floor($raList[$i]->Size / $this->_BIG_BLOCK_SIZE) + + (($raList[$i]->Size % $this->_BIG_BLOCK_SIZE)? 1: 0)); + } + // Close file for each PPS, and unlink it + if (isset($raList[$i]->_PPS_FILE)) + { + @fclose($raList[$i]->_PPS_FILE); + $raList[$i]->_PPS_FILE = null; + @unlink($raList[$i]->_tmp_filename); + } + } + } + } + + /** + * get small data (PPS's with data smaller than OLE_DATA_SIZE_SMALL) + * + * @access private + * @param array &$raList Reference to array of PPS's + */ + function _makeSmallData(&$raList) + { + $sRes = ''; + $FILE = $this->_FILEH_; + $iSmBlk = 0; + + for ($i = 0; $i < count($raList); $i++) + { + // Make SBD, small data string + if ($raList[$i]->Type == OLE_PPS_TYPE_FILE) + { + if ($raList[$i]->Size <= 0) { + continue; + } + if ($raList[$i]->Size < OLE_DATA_SIZE_SMALL) + { + $iSmbCnt = floor($raList[$i]->Size / $this->_SMALL_BLOCK_SIZE) + + (($raList[$i]->Size % $this->_SMALL_BLOCK_SIZE)? 1: 0); + // Add to SBD + for ($j = 0; $j < ($iSmbCnt-1); $j++) { + fwrite($FILE, pack("V", $j+$iSmBlk+1)); + } + fwrite($FILE, pack("V", -2)); + + // Add to Data String(this will be written for RootEntry) + if ($raList[$i]->_PPS_FILE) + { + fseek($raList[$i]->_PPS_FILE, 0); // To The Top + while ($sBuff = fread($raList[$i]->_PPS_FILE, 4096)) { + $sRes .= $sBuff; + } + } + else { + $sRes .= $raList[$i]->_data; + } + if($raList[$i]->Size % $this->_SMALL_BLOCK_SIZE) + { + for ($j = 0; $j < ($this->_SMALL_BLOCK_SIZE - ($raList[$i]->Size % $this->_SMALL_BLOCK_SIZE)); $j++) { + $sRes .= "\x00"; + } + } + // Set for PPS + $raList[$i]->_StartBlock = $iSmBlk; + $iSmBlk += $iSmbCnt; + } + } + } + $iSbCnt = floor($this->_BIG_BLOCK_SIZE / OLE_LONG_INT_SIZE); + if($iSmBlk % $iSbCnt) + { + for ($i = 0; $i < ($iSbCnt - ($iSmBlk % $iSbCnt)); $i++) { + fwrite($FILE, pack("V", -1)); + } + } + return $sRes; + } + + /** + * Saves all the PPS's WKs + * + * @access private + * @param array $raList Reference to an array with all PPS's + */ + function _savePps(&$raList) + { + // Save each PPS WK + for ($i = 0; $i < count($raList); $i++) { + fwrite($this->_FILEH_, $raList[$i]->_getPpsWk()); + } + // Adjust for Block + $iCnt = count($raList); + $iBCnt = $this->_BIG_BLOCK_SIZE / OLE_PPS_SIZE; + if ($iCnt % $iBCnt) + { + for ($i = 0; $i < (($iBCnt - ($iCnt % $iBCnt)) * OLE_PPS_SIZE); $i++) { + fwrite($this->_FILEH_, "\x00"); + } + } + } + + /** + * Saving Big Block Depot + * + * @access private + * @param integer $iSbdSize + * @param integer $iBsize + * @param integer $iPpsCnt + */ + function _saveBbd($iSbdSize, $iBsize, $iPpsCnt) + { + $FILE = $this->_FILEH_; + // Calculate Basic Setting + $iBbCnt = $this->_BIG_BLOCK_SIZE / OLE_LONG_INT_SIZE; + $i1stBdL = ($this->_BIG_BLOCK_SIZE - 0x4C) / OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBsize + $iPpsCnt + $iSbdSize; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt)? 1: 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW+$iBdCntW) % $iBbCnt)? 1: 0); + // Calculate BD count + if ($iBdCnt >$i1stBdL) + { + while (1) + { + $iBdExL++; + $iAllW++; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt)? 1: 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW+$iBdCntW) % $iBbCnt)? 1: 0); + if ($iBdCnt <= ($iBdExL*$iBbCnt+ $i1stBdL)) { + break; + } + } + } + + // Making BD + // Set for SBD + if ($iSbdSize > 0) + { + for ($i = 0; $i<($iSbdSize-1); $i++) { + fwrite($FILE, pack("V", $i+1)); + } + fwrite($FILE, pack("V", -2)); + } + // Set for B + for ($i = 0; $i<($iBsize-1); $i++) { + fwrite($FILE, pack("V", $i+$iSbdSize+1)); + } + fwrite($FILE, pack("V", -2)); + + // Set for PPS + for ($i = 0; $i<($iPpsCnt-1); $i++) { + fwrite($FILE, pack("V", $i+$iSbdSize+$iBsize+1)); + } + fwrite($FILE, pack("V", -2)); + // Set for BBD itself ( 0xFFFFFFFD : BBD) + for ($i=0; $i<$iBdCnt;$i++) { + fwrite($FILE, pack("V", 0xFFFFFFFD)); + } + // Set for ExtraBDList + for ($i=0; $i<$iBdExL;$i++) { + fwrite($FILE, pack("V", 0xFFFFFFFC)); + } + // Adjust for Block + if (($iAllW + $iBdCnt) % $iBbCnt) + { + for ($i = 0; $i < ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt)); $i++) { + fwrite($FILE, pack("V", -1)); + } + } + // Extra BDList + if ($iBdCnt > $i1stBdL) + { + $iN=0; + $iNb=0; + for ($i=$i1stBdL;$i<$iBdCnt; $i++, $iN++) + { + if ($iN>=($iBbCnt-1)) + { + $iN = 0; + $iNb++; + fwrite($FILE, pack("V", $iAll+$iBdCnt+$iNb)); + } + fwrite($FILE, pack("V", $iBsize+$iSbdSize+$iPpsCnt+$i)); + } + if (($iBdCnt-$i1stBdL) % ($iBbCnt-1)) + { + for ($i = 0; $i < (($iBbCnt-1) - (($iBdCnt-$i1stBdL) % ($iBbCnt-1))); $i++) { + fwrite($FILE, pack("V", -1)); + } + } + fwrite($FILE, pack("V", -2)); + } + } +} +?> diff --git a/OS/Guess.php b/OS/Guess.php new file mode 100644 index 0000000..4eb6173 --- /dev/null +++ b/OS/Guess.php @@ -0,0 +1,344 @@ + + * @author Gregory Beaver + * @copyright 1997-2005 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Guess.php,v 1.25 2006/12/14 00:24:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since PEAR 0.1 + */ + +// {{{ uname examples + +// php_uname() without args returns the same as 'uname -a', or a PHP-custom +// string for Windows. +// PHP versions prior to 4.3 return the uname of the host where PHP was built, +// as of 4.3 it returns the uname of the host running the PHP code. +// +// PC RedHat Linux 7.1: +// Linux host.example.com 2.4.2-2 #1 Sun Apr 8 20:41:30 EDT 2001 i686 unknown +// +// PC Debian Potato: +// Linux host 2.4.17 #2 SMP Tue Feb 12 15:10:04 CET 2002 i686 unknown +// +// PC FreeBSD 3.3: +// FreeBSD host.example.com 3.3-STABLE FreeBSD 3.3-STABLE #0: Mon Feb 21 00:42:31 CET 2000 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.3: +// FreeBSD host.example.com 4.3-RELEASE FreeBSD 4.3-RELEASE #1: Mon Jun 25 11:19:43 EDT 2001 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.5: +// FreeBSD host.example.com 4.5-STABLE FreeBSD 4.5-STABLE #0: Wed Feb 6 23:59:23 CET 2002 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.5 w/uname from GNU shellutils: +// FreeBSD host.example.com 4.5-STABLE FreeBSD 4.5-STABLE #0: Wed Feb i386 unknown +// +// HP 9000/712 HP-UX 10: +// HP-UX iq B.10.10 A 9000/712 2008429113 two-user license +// +// HP 9000/712 HP-UX 10 w/uname from GNU shellutils: +// HP-UX host B.10.10 A 9000/712 unknown +// +// IBM RS6000/550 AIX 4.3: +// AIX host 3 4 000003531C00 +// +// AIX 4.3 w/uname from GNU shellutils: +// AIX host 3 4 000003531C00 unknown +// +// SGI Onyx IRIX 6.5 w/uname from GNU shellutils: +// IRIX64 host 6.5 01091820 IP19 mips +// +// SGI Onyx IRIX 6.5: +// IRIX64 host 6.5 01091820 IP19 +// +// SparcStation 20 Solaris 8 w/uname from GNU shellutils: +// SunOS host.example.com 5.8 Generic_108528-12 sun4m sparc +// +// SparcStation 20 Solaris 8: +// SunOS host.example.com 5.8 Generic_108528-12 sun4m sparc SUNW,SPARCstation-20 +// +// Mac OS X (Darwin) +// Darwin home-eden.local 7.5.0 Darwin Kernel Version 7.5.0: Thu Aug 5 19:26:16 PDT 2004; root:xnu/xnu-517.7.21.obj~3/RELEASE_PPC Power Macintosh +// +// Mac OS X early versions +// + +// }}} + +/* TODO: + * - define endianness, to allow matchSignature("bigend") etc. + */ + +/** + * Retrieves information about the current operating system + * + * This class uses php_uname() to grok information about the current OS + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Gregory Beaver + * @copyright 1997-2005 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class OS_Guess +{ + var $sysname; + var $nodename; + var $cpu; + var $release; + var $extra; + + function OS_Guess($uname = null) + { + list($this->sysname, + $this->release, + $this->cpu, + $this->extra, + $this->nodename) = $this->parseSignature($uname); + } + + function parseSignature($uname = null) + { + static $sysmap = array( + 'HP-UX' => 'hpux', + 'IRIX64' => 'irix', + ); + static $cpumap = array( + 'i586' => 'i386', + 'i686' => 'i386', + 'ppc' => 'powerpc', + ); + if ($uname === null) { + $uname = php_uname(); + } + $parts = split('[[:space:]]+', trim($uname)); + $n = count($parts); + + $release = $machine = $cpu = ''; + $sysname = $parts[0]; + $nodename = $parts[1]; + $cpu = $parts[$n-1]; + $extra = ''; + if ($cpu == 'unknown') { + $cpu = $parts[$n-2]; + } + + switch ($sysname) { + case 'AIX' : + $release = "$parts[3].$parts[2]"; + break; + case 'Windows' : + switch ($parts[1]) { + case '95/98': + $release = '9x'; + break; + default: + $release = $parts[1]; + break; + } + $cpu = 'i386'; + break; + case 'Linux' : + $extra = $this->_detectGlibcVersion(); + // use only the first two digits from the kernel version + $release = ereg_replace('^([[:digit:]]+\.[[:digit:]]+).*', '\1', $parts[2]); + break; + case 'Mac' : + $sysname = 'darwin'; + $nodename = $parts[2]; + $release = $parts[3]; + if ($cpu == 'Macintosh') { + if ($parts[$n - 2] == 'Power') { + $cpu = 'powerpc'; + } + } + break; + case 'Darwin' : + if ($cpu == 'Macintosh') { + if ($parts[$n - 2] == 'Power') { + $cpu = 'powerpc'; + } + } + $release = ereg_replace('^([[:digit:]]+\.[[:digit:]]+).*', '\1', $parts[2]); + break; + default: + $release = ereg_replace('-.*', '', $parts[2]); + break; + } + + + if (isset($sysmap[$sysname])) { + $sysname = $sysmap[$sysname]; + } else { + $sysname = strtolower($sysname); + } + if (isset($cpumap[$cpu])) { + $cpu = $cpumap[$cpu]; + } + return array($sysname, $release, $cpu, $extra, $nodename); + } + + function _detectGlibcVersion() + { + static $glibc = false; + if ($glibc !== false) { + return $glibc; // no need to run this multiple times + } + $major = $minor = 0; + include_once "System.php"; + // Use glibc's header file to + // get major and minor version number: + if (@file_exists('/usr/include/features.h') && + @is_readable('/usr/include/features.h')) { + if (!@file_exists('/usr/bin/cpp') || !@is_executable('/usr/bin/cpp')) { + $features_file = fopen('/usr/include/features.h', 'rb'); + while (!feof($features_file)) { + $line = fgets($features_file, 8192); + if (!$line || (strpos($line, '#define') === false)) { + continue; + } + if (strpos($line, '__GLIBC__')) { + // major version number #define __GLIBC__ version + $line = preg_split('/\s+/', $line); + $glibc_major = trim($line[2]); + if (isset($glibc_minor)) { + break; + } + continue; + } + if (strpos($line, '__GLIBC_MINOR__')) { + // got the minor version number + // #define __GLIBC_MINOR__ version + $line = preg_split('/\s+/', $line); + $glibc_minor = trim($line[2]); + if (isset($glibc_major)) { + break; + } + continue; + } + } + fclose($features_file); + if (!isset($glibc_major) || !isset($glibc_minor)) { + return $glibc = ''; + } + return $glibc = 'glibc' . trim($glibc_major) . "." . trim($glibc_minor) ; + } // no cpp + $tmpfile = System::mktemp("glibctest"); + $fp = fopen($tmpfile, "w"); + fwrite($fp, "#include \n__GLIBC__ __GLIBC_MINOR__\n"); + fclose($fp); + $cpp = popen("/usr/bin/cpp $tmpfile", "r"); + while ($line = fgets($cpp, 1024)) { + if ($line{0} == '#' || trim($line) == '') { + continue; + } + if (list($major, $minor) = explode(' ', trim($line))) { + break; + } + } + pclose($cpp); + unlink($tmpfile); + } // features.h + if (!($major && $minor) && @is_link('/lib/libc.so.6')) { + // Let's try reading the libc.so.6 symlink + if (ereg('^libc-(.*)\.so$', basename(readlink('/lib/libc.so.6')), $matches)) { + list($major, $minor) = explode('.', $matches[1]); + } + } + if (!($major && $minor)) { + return $glibc = ''; + } + return $glibc = "glibc{$major}.{$minor}"; + } + + function getSignature() + { + if (empty($this->extra)) { + return "{$this->sysname}-{$this->release}-{$this->cpu}"; + } + return "{$this->sysname}-{$this->release}-{$this->cpu}-{$this->extra}"; + } + + function getSysname() + { + return $this->sysname; + } + + function getNodename() + { + return $this->nodename; + } + + function getCpu() + { + return $this->cpu; + } + + function getRelease() + { + return $this->release; + } + + function getExtra() + { + return $this->extra; + } + + function matchSignature($match) + { + if (is_array($match)) { + $fragments = $match; + } else { + $fragments = explode('-', $match); + } + $n = count($fragments); + $matches = 0; + if ($n > 0) { + $matches += $this->_matchFragment($fragments[0], $this->sysname); + } + if ($n > 1) { + $matches += $this->_matchFragment($fragments[1], $this->release); + } + if ($n > 2) { + $matches += $this->_matchFragment($fragments[2], $this->cpu); + } + if ($n > 3) { + $matches += $this->_matchFragment($fragments[3], $this->extra); + } + return ($matches == $n); + } + + function _matchFragment($fragment, $value) + { + if (strcspn($fragment, '*?') < strlen($fragment)) { + $reg = '^' . str_replace(array('*', '?', '/'), array('.*', '.', '\\/'), $fragment) . '$'; + return eregi($reg, $value); + } + return ($fragment == '*' || !strcasecmp($fragment, $value)); + } + +} +/* + * Local Variables: + * indent-tabs-mode: nil + * c-basic-offset: 4 + * End: + */ +?> diff --git a/PEAR.php b/PEAR.php new file mode 100644 index 0000000..fc879a0 --- /dev/null +++ b/PEAR.php @@ -0,0 +1,1108 @@ + + * @author Stig Bakken + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: PEAR.php,v 1.101 2006/04/25 02:41:03 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/**#@+ + * ERROR constants + */ +define('PEAR_ERROR_RETURN', 1); +define('PEAR_ERROR_PRINT', 2); +define('PEAR_ERROR_TRIGGER', 4); +define('PEAR_ERROR_DIE', 8); +define('PEAR_ERROR_CALLBACK', 16); +/** + * WARNING: obsolete + * @deprecated + */ +define('PEAR_ERROR_EXCEPTION', 32); +/**#@-*/ +define('PEAR_ZE2', (function_exists('version_compare') && + version_compare(zend_version(), "2-dev", "ge"))); + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); + define('OS_UNIX', false); + define('PEAR_OS', 'Windows'); +} else { + define('OS_WINDOWS', false); + define('OS_UNIX', true); + define('PEAR_OS', 'Unix'); // blatant assumption +} + +// instant backwards compatibility +if (!defined('PATH_SEPARATOR')) { + if (OS_WINDOWS) { + define('PATH_SEPARATOR', ';'); + } else { + define('PATH_SEPARATOR', ':'); + } +} + +$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; +$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; +$GLOBALS['_PEAR_destructor_object_list'] = array(); +$GLOBALS['_PEAR_shutdown_funcs'] = array(); +$GLOBALS['_PEAR_error_handler_stack'] = array(); + +@ini_set('track_errors', true); + +/** + * Base class for other PEAR classes. Provides rudimentary + * emulation of destructors. + * + * If you want a destructor in your class, inherit PEAR and make a + * destructor method called _yourclassname (same name as the + * constructor, but with a "_" prefix). Also, in your constructor you + * have to call the PEAR constructor: $this->PEAR();. + * The destructor method will be called without parameters. Note that + * at in some SAPI implementations (such as Apache), any output during + * the request shutdown (in which destructors are called) seems to be + * discarded. If you need to get any debug information from your + * destructor, use error_log(), syslog() or something similar. + * + * IMPORTANT! To use the emulated destructors you need to create the + * objects by reference: $obj =& new PEAR_child; + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @see PEAR_Error + * @since Class available since PHP 4.0.2 + * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear + */ +class PEAR +{ + // {{{ properties + + /** + * Whether to enable internal debug messages. + * + * @var bool + * @access private + */ + var $_debug = false; + + /** + * Default error mode for this object. + * + * @var int + * @access private + */ + var $_default_error_mode = null; + + /** + * Default error options used for this object when error mode + * is PEAR_ERROR_TRIGGER. + * + * @var int + * @access private + */ + var $_default_error_options = null; + + /** + * Default error handler (callback) for this object, if error mode is + * PEAR_ERROR_CALLBACK. + * + * @var string + * @access private + */ + var $_default_error_handler = ''; + + /** + * Which class to use for error objects. + * + * @var string + * @access private + */ + var $_error_class = 'PEAR_Error'; + + /** + * An array of expected errors. + * + * @var array + * @access private + */ + var $_expected_errors = array(); + + // }}} + + // {{{ constructor + + /** + * Constructor. Registers this object in + * $_PEAR_destructor_object_list for destructor emulation if a + * destructor object exists. + * + * @param string $error_class (optional) which class to use for + * error objects, defaults to PEAR_Error. + * @access public + * @return void + */ + function PEAR($error_class = null) + { + $classname = strtolower(get_class($this)); + if ($this->_debug) { + print "PEAR constructor called, class=$classname\n"; + } + if ($error_class !== null) { + $this->_error_class = $error_class; + } + while ($classname && strcasecmp($classname, "pear")) { + $destructor = "_$classname"; + if (method_exists($this, $destructor)) { + global $_PEAR_destructor_object_list; + $_PEAR_destructor_object_list[] = &$this; + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + break; + } else { + $classname = get_parent_class($classname); + } + } + } + + // }}} + // {{{ destructor + + /** + * Destructor (the emulated type of...). Does nothing right now, + * but is included for forward compatibility, so subclass + * destructors should always call it. + * + * See the note in the class desciption about output from + * destructors. + * + * @access public + * @return void + */ + function _PEAR() { + if ($this->_debug) { + printf("PEAR destructor called, class=%s\n", strtolower(get_class($this))); + } + } + + // }}} + // {{{ getStaticProperty() + + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + return $properties[$class][$var]; + } + + // }}} + // {{{ registerShutdownFunc() + + /** + * Use this function to register a shutdown method for static + * classes. + * + * @access public + * @param mixed $func The function name (or array of class/method) to call + * @param mixed $args The arguments to pass to the function + * @return void + */ + function registerShutdownFunc($func, $args = array()) + { + // if we are called statically, there is a potential + // that no shutdown func is registered. Bug #6445 + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); + } + + // }}} + // {{{ isError() + + /** + * Tell whether a value is a PEAR error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true + * only if $code is a string and + * $obj->getMessage() == $code or + * $code is an integer and $obj->getCode() == $code + * @access public + * @return bool true if parameter is an error + */ + function isError($data, $code = null) + { + if (is_a($data, 'PEAR_Error')) { + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } else { + return $data->getCode() == $code; + } + } + return false; + } + + // }}} + // {{{ setErrorHandling() + + /** + * Sets how errors generated by this object should be handled. + * Can be invoked both in objects and statically. If called + * statically, setErrorHandling sets the default behaviour for all + * PEAR objects. If called in an object, setErrorHandling sets + * the default behaviour for that object. + * + * @param int $mode + * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION. + * + * @param mixed $options + * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one + * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * + * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected + * to be the callback function or method. A callback + * function is a string with the name of the function, a + * callback method is an array of two elements: the element + * at index 0 is the object, and the element at index 1 is + * the name of the method to call in the object. + * + * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is + * a printf format string used when printing the error + * message. + * + * @access public + * @return void + * @see PEAR_ERROR_RETURN + * @see PEAR_ERROR_PRINT + * @see PEAR_ERROR_TRIGGER + * @see PEAR_ERROR_DIE + * @see PEAR_ERROR_CALLBACK + * @see PEAR_ERROR_EXCEPTION + * + * @since PHP 4.0.5 + */ + + function setErrorHandling($mode = null, $options = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $setmode = &$this->_default_error_mode; + $setoptions = &$this->_default_error_options; + } else { + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + } + + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + } + + // }}} + // {{{ expectError() + + /** + * This method is used to tell which errors you expect to get. + * Expected errors are always returned with error mode + * PEAR_ERROR_RETURN. Expected error codes are stored in a stack, + * and this method pushes a new element onto it. The list of + * expected errors are in effect until they are popped off the + * stack with the popExpect() method. + * + * Note that this method can not be called statically + * + * @param mixed $code a single error code or an array of error codes to expect + * + * @return int the new depth of the "expected errors" stack + * @access public + */ + function expectError($code = '*') + { + if (is_array($code)) { + array_push($this->_expected_errors, $code); + } else { + array_push($this->_expected_errors, array($code)); + } + return sizeof($this->_expected_errors); + } + + // }}} + // {{{ popExpect() + + /** + * This method pops one element off the expected error codes + * stack. + * + * @return array the list of error codes that were popped + */ + function popExpect() + { + return array_pop($this->_expected_errors); + } + + // }}} + // {{{ _checkDelExpect() + + /** + * This method checks unsets an error code if available + * + * @param mixed error code + * @return bool true if the error code was unset, false otherwise + * @access private + * @since PHP 4.3.0 + */ + function _checkDelExpect($error_code) + { + $deleted = false; + + foreach ($this->_expected_errors AS $key => $error_array) { + if (in_array($error_code, $error_array)) { + unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); + $deleted = true; + } + + // clean up empty arrays + if (0 == count($this->_expected_errors[$key])) { + unset($this->_expected_errors[$key]); + } + } + return $deleted; + } + + // }}} + // {{{ delExpect() + + /** + * This method deletes all occurences of the specified element from + * the expected error codes stack. + * + * @param mixed $error_code error code that should be deleted + * @return mixed list of error codes that were deleted or error + * @access public + * @since PHP 4.3.0 + */ + function delExpect($error_code) + { + $deleted = false; + + if ((is_array($error_code) && (0 != count($error_code)))) { + // $error_code is a non-empty array here; + // we walk through it trying to unset all + // values + foreach($error_code as $key => $error) { + if ($this->_checkDelExpect($error)) { + $deleted = true; + } else { + $deleted = false; + } + } + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } elseif (!empty($error_code)) { + // $error_code comes alone, trying to unset it + if ($this->_checkDelExpect($error_code)) { + return true; + } else { + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + } else { + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } + } + + // }}} + // {{{ raiseError() + + /** + * This method is a wrapper that returns an instance of the + * configured error class with this object's default error + * handling applied. If the $mode and $options parameters are not + * specified, the object's defaults are used. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION. + * + * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter + * specifies the PHP-internal error level (one of + * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * If $mode is PEAR_ERROR_CALLBACK, this + * parameter specifies the callback function or + * method. In other error modes this parameter + * is ignored. + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @param string $error_class The returned error object will be + * instantiated from this class, if specified. + * + * @param bool $skipmsg If true, raiseError will only pass error codes, + * the error message parameter will be dropped. + * + * @access public + * @return object a PEAR error object + * @see PEAR::setErrorHandling + * @since PHP 4.0.5 + */ + function &raiseError($message = null, + $code = null, + $mode = null, + $options = null, + $userinfo = null, + $error_class = null, + $skipmsg = false) + { + // The error is yet a PEAR error object + if (is_object($message)) { + $code = $message->getCode(); + $userinfo = $message->getUserInfo(); + $error_class = $message->getType(); + $message->error_message_prefix = ''; + $message = $message->getMessage(); + } + + if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ($exp[0] == "*" || + (is_int(reset($exp)) && in_array($code, $exp)) || + (is_string(reset($exp)) && in_array($message, $exp))) { + $mode = PEAR_ERROR_RETURN; + } + } + // No mode given, try global ones + if ($mode === null) { + // Class error handler + if (isset($this) && isset($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + // Global error handler + } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) { + $mode = $GLOBALS['_PEAR_default_error_mode']; + $options = $GLOBALS['_PEAR_default_error_options']; + } + } + + if ($error_class !== null) { + $ec = $error_class; + } elseif (isset($this) && isset($this->_error_class)) { + $ec = $this->_error_class; + } else { + $ec = 'PEAR_Error'; + } + if ($skipmsg) { + $a = &new $ec($code, $mode, $options, $userinfo); + return $a; + } else { + $a = &new $ec($message, $code, $mode, $options, $userinfo); + return $a; + } + } + + // }}} + // {{{ throwError() + + /** + * Simpler form of raiseError with fewer options. In most cases + * message, code and userinfo are enough. + * + * @param string $message + * + */ + function &throwError($message = null, + $code = null, + $userinfo = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $a = &$this->raiseError($message, $code, null, null, $userinfo); + return $a; + } else { + $a = &PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; + } + } + + // }}} + function staticPushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + $stack[] = array($def_mode, $def_options); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $def_mode = $mode; + $def_options = $options; + break; + + case PEAR_ERROR_CALLBACK: + $def_mode = $mode; + // class/object method callback + if (is_callable($options)) { + $def_options = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + $stack[] = array($mode, $options); + return true; + } + + function staticPopErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + return true; + } + + // {{{ pushErrorHandling() + + /** + * Push a new error handler on top of the error handler options stack. With this + * you can easily override the actual error handler for some code and restore + * it later with popErrorHandling. + * + * @param mixed $mode (same as setErrorHandling) + * @param mixed $options (same as setErrorHandling) + * + * @return bool Always true + * + * @see PEAR::setErrorHandling + */ + function pushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + if (isset($this) && is_a($this, 'PEAR')) { + $def_mode = &$this->_default_error_mode; + $def_options = &$this->_default_error_options; + } else { + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + } + $stack[] = array($def_mode, $def_options); + + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + $stack[] = array($mode, $options); + return true; + } + + // }}} + // {{{ popErrorHandling() + + /** + * Pop the last error handler used + * + * @return bool Always true + * + * @see PEAR::pushErrorHandling + */ + function popErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + return true; + } + + // }}} + // {{{ loadExtension() + + /** + * OS independant PHP extension load. Remember to take care + * on the correct extension name for case sensitive OSes. + * + * @param string $ext The extension name + * @return bool Success or not on the dl() call + */ + function loadExtension($ext) + { + if (!extension_loaded($ext)) { + // if either returns true dl() will produce a FATAL error, stop that + if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { + return false; + } + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } + return true; + } + + // }}} +} + +// {{{ _PEAR_call_destructors() + +function _PEAR_call_destructors() +{ + global $_PEAR_destructor_object_list; + if (is_array($_PEAR_destructor_object_list) && + sizeof($_PEAR_destructor_object_list)) + { + reset($_PEAR_destructor_object_list); + if (PEAR::getStaticProperty('PEAR', 'destructlifo')) { + $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); + } + while (list($k, $objref) = each($_PEAR_destructor_object_list)) { + $classname = get_class($objref); + while ($classname) { + $destructor = "_$classname"; + if (method_exists($objref, $destructor)) { + $objref->$destructor(); + break; + } else { + $classname = get_parent_class($classname); + } + } + } + // Empty the object list to ensure that destructors are + // not called more than once. + $_PEAR_destructor_object_list = array(); + } + + // Now call the shutdown functions + if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { + call_user_func_array($value[0], $value[1]); + } + } +} + +// }}} +/** + * Standard PEAR error class for PHP 4 + * + * This class is supserseded by {@link PEAR_Exception} in PHP 5 + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V. Cox + * @author Gregory Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/manual/en/core.pear.pear-error.php + * @see PEAR::raiseError(), PEAR::throwError() + * @since Class available since PHP 4.0.2 + */ +class PEAR_Error +{ + // {{{ properties + + var $error_message_prefix = ''; + var $mode = PEAR_ERROR_RETURN; + var $level = E_USER_NOTICE; + var $code = -1; + var $message = ''; + var $userinfo = ''; + var $backtrace = null; + + // }}} + // {{{ constructor + + /** + * PEAR_Error constructor + * + * @param string $message message + * + * @param int $code (optional) error code + * + * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN, + * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION + * + * @param mixed $options (optional) error level, _OR_ in the case of + * PEAR_ERROR_CALLBACK, the callback function or object/method + * tuple. + * + * @param string $userinfo (optional) additional user/debug info + * + * @access public + * + */ + function PEAR_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + if ($mode === null) { + $mode = PEAR_ERROR_RETURN; + } + $this->message = $message; + $this->code = $code; + $this->mode = $mode; + $this->userinfo = $userinfo; + if (!PEAR::getStaticProperty('PEAR_Error', 'skiptrace')) { + $this->backtrace = debug_backtrace(); + if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { + unset($this->backtrace[0]['object']); + } + } + if ($mode & PEAR_ERROR_CALLBACK) { + $this->level = E_USER_NOTICE; + $this->callback = $options; + } else { + if ($options === null) { + $options = E_USER_NOTICE; + } + $this->level = $options; + $this->callback = null; + } + if ($this->mode & PEAR_ERROR_PRINT) { + if (is_null($options) || is_int($options)) { + $format = "%s"; + } else { + $format = $options; + } + printf($format, $this->getMessage()); + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + trigger_error($this->getMessage(), $this->level); + } + if ($this->mode & PEAR_ERROR_DIE) { + $msg = $this->getMessage(); + if (is_null($options) || is_int($options)) { + $format = "%s"; + if (substr($msg, -1) != "\n") { + $msg .= "\n"; + } + } else { + $format = $options; + } + die(sprintf($format, $msg)); + } + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_callable($this->callback)) { + call_user_func($this->callback, $this); + } + } + if ($this->mode & PEAR_ERROR_EXCEPTION) { + trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); + eval('$e = new Exception($this->message, $this->code);throw($e);'); + } + } + + // }}} + // {{{ getMode() + + /** + * Get the error mode from an error object. + * + * @return int error mode + * @access public + */ + function getMode() { + return $this->mode; + } + + // }}} + // {{{ getCallback() + + /** + * Get the callback function/method from an error object. + * + * @return mixed callback function or object/method array + * @access public + */ + function getCallback() { + return $this->callback; + } + + // }}} + // {{{ getMessage() + + + /** + * Get the error message from an error object. + * + * @return string full error message + * @access public + */ + function getMessage() + { + return ($this->error_message_prefix . $this->message); + } + + + // }}} + // {{{ getCode() + + /** + * Get error code from an error object + * + * @return int error code + * @access public + */ + function getCode() + { + return $this->code; + } + + // }}} + // {{{ getType() + + /** + * Get the name of this error/exception. + * + * @return string error/exception name (type) + * @access public + */ + function getType() + { + return get_class($this); + } + + // }}} + // {{{ getUserInfo() + + /** + * Get additional user-supplied information. + * + * @return string user-supplied information + * @access public + */ + function getUserInfo() + { + return $this->userinfo; + } + + // }}} + // {{{ getDebugInfo() + + /** + * Get additional debug information supplied by the application. + * + * @return string debug information + * @access public + */ + function getDebugInfo() + { + return $this->getUserInfo(); + } + + // }}} + // {{{ getBacktrace() + + /** + * Get the call backtrace from where the error was generated. + * Supported with PHP 4.3.0 or newer. + * + * @param int $frame (optional) what frame to fetch + * @return array Backtrace, or NULL if not available. + * @access public + */ + function getBacktrace($frame = null) + { + if (defined('PEAR_IGNORE_BACKTRACE')) { + return null; + } + if ($frame === null) { + return $this->backtrace; + } + return $this->backtrace[$frame]; + } + + // }}} + // {{{ addUserInfo() + + function addUserInfo($info) + { + if (empty($this->userinfo)) { + $this->userinfo = $info; + } else { + $this->userinfo .= " ** $info"; + } + } + + // }}} + // {{{ toString() + + /** + * Make a string representation of this object. + * + * @return string a string with an object summary + * @access public + */ + function toString() { + $modes = array(); + $levels = array(E_USER_NOTICE => 'notice', + E_USER_WARNING => 'warning', + E_USER_ERROR => 'error'); + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_array($this->callback)) { + $callback = (is_object($this->callback[0]) ? + strtolower(get_class($this->callback[0])) : + $this->callback[0]) . '::' . + $this->callback[1]; + } else { + $callback = $this->callback; + } + return sprintf('[%s: message="%s" code=%d mode=callback '. + 'callback=%s prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + $callback, $this->error_message_prefix, + $this->userinfo); + } + if ($this->mode & PEAR_ERROR_PRINT) { + $modes[] = 'print'; + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + $modes[] = 'trigger'; + } + if ($this->mode & PEAR_ERROR_DIE) { + $modes[] = 'die'; + } + if ($this->mode & PEAR_ERROR_RETURN) { + $modes[] = 'return'; + } + return sprintf('[%s: message="%s" code=%d mode=%s level=%s '. + 'prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + implode("|", $modes), $levels[$this->level], + $this->error_message_prefix, + $this->userinfo); + } + + // }}} +} + +/* + * Local Variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ +?> diff --git a/PEAR/Autoloader.php b/PEAR/Autoloader.php new file mode 100644 index 0000000..6281b27 --- /dev/null +++ b/PEAR/Autoloader.php @@ -0,0 +1,223 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Autoloader.php,v 1.13 2006/01/06 04:47:36 cellog Exp $ + * @link http://pear.php.net/manual/en/core.ppm.php#core.ppm.pear-autoloader + * @since File available since Release 0.1 + * @deprecated File deprecated in Release 1.4.0a1 + */ + +// /* vim: set expandtab tabstop=4 shiftwidth=4: */ + +if (!extension_loaded("overload")) { + // die hard without ext/overload + die("Rebuild PHP with the `overload' extension to use PEAR_Autoloader"); +} + +/** + * Include for PEAR_Error and PEAR classes + */ +require_once "PEAR.php"; + +/** + * This class is for objects where you want to separate the code for + * some methods into separate classes. This is useful if you have a + * class with not-frequently-used methods that contain lots of code + * that you would like to avoid always parsing. + * + * The PEAR_Autoloader class provides autoloading and aggregation. + * The autoloading lets you set up in which classes the separated + * methods are found. Aggregation is the technique used to import new + * methods, an instance of each class providing separated methods is + * stored and called every time the aggregated method is called. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/manual/en/core.ppm.php#core.ppm.pear-autoloader + * @since File available since Release 0.1 + * @deprecated File deprecated in Release 1.4.0a1 + */ +class PEAR_Autoloader extends PEAR +{ + // {{{ properties + + /** + * Map of methods and classes where they are defined + * + * @var array + * + * @access private + */ + var $_autoload_map = array(); + + /** + * Map of methods and aggregate objects + * + * @var array + * + * @access private + */ + var $_method_map = array(); + + // }}} + // {{{ addAutoload() + + /** + * Add one or more autoload entries. + * + * @param string $method which method to autoload + * + * @param string $classname (optional) which class to find the method in. + * If the $method parameter is an array, this + * parameter may be omitted (and will be ignored + * if not), and the $method parameter will be + * treated as an associative array with method + * names as keys and class names as values. + * + * @return void + * + * @access public + */ + function addAutoload($method, $classname = null) + { + if (is_array($method)) { + array_walk($method, create_function('$a,&$b', '$b = strtolower($b);')); + $this->_autoload_map = array_merge($this->_autoload_map, $method); + } else { + $this->_autoload_map[strtolower($method)] = $classname; + } + } + + // }}} + // {{{ removeAutoload() + + /** + * Remove an autoload entry. + * + * @param string $method which method to remove the autoload entry for + * + * @return bool TRUE if an entry was removed, FALSE if not + * + * @access public + */ + function removeAutoload($method) + { + $method = strtolower($method); + $ok = isset($this->_autoload_map[$method]); + unset($this->_autoload_map[$method]); + return $ok; + } + + // }}} + // {{{ addAggregateObject() + + /** + * Add an aggregate object to this object. If the specified class + * is not defined, loading it will be attempted following PEAR's + * file naming scheme. All the methods in the class will be + * aggregated, except private ones (name starting with an + * underscore) and constructors. + * + * @param string $classname what class to instantiate for the object. + * + * @return void + * + * @access public + */ + function addAggregateObject($classname) + { + $classname = strtolower($classname); + if (!class_exists($classname)) { + $include_file = preg_replace('/[^a-z0-9]/i', '_', $classname); + include_once $include_file; + } + $obj =& new $classname; + $methods = get_class_methods($classname); + foreach ($methods as $method) { + // don't import priviate methods and constructors + if ($method{0} != '_' && $method != $classname) { + $this->_method_map[$method] = $obj; + } + } + } + + // }}} + // {{{ removeAggregateObject() + + /** + * Remove an aggregate object. + * + * @param string $classname the class of the object to remove + * + * @return bool TRUE if an object was removed, FALSE if not + * + * @access public + */ + function removeAggregateObject($classname) + { + $ok = false; + $classname = strtolower($classname); + reset($this->_method_map); + while (list($method, $obj) = each($this->_method_map)) { + if (is_a($obj, $classname)) { + unset($this->_method_map[$method]); + $ok = true; + } + } + return $ok; + } + + // }}} + // {{{ __call() + + /** + * Overloaded object call handler, called each time an + * undefined/aggregated method is invoked. This method repeats + * the call in the right aggregate object and passes on the return + * value. + * + * @param string $method which method that was called + * + * @param string $args An array of the parameters passed in the + * original call + * + * @return mixed The return value from the aggregated method, or a PEAR + * error if the called method was unknown. + */ + function __call($method, $args, &$retval) + { + $method = strtolower($method); + if (empty($this->_method_map[$method]) && isset($this->_autoload_map[$method])) { + $this->addAggregateObject($this->_autoload_map[$method]); + } + if (isset($this->_method_map[$method])) { + $retval = call_user_func_array(array($this->_method_map[$method], $method), $args); + return true; + } + return false; + } + + // }}} +} + +overload("PEAR_Autoloader"); + +?> diff --git a/PEAR/Builder.php b/PEAR/Builder.php new file mode 100644 index 0000000..0cc932e --- /dev/null +++ b/PEAR/Builder.php @@ -0,0 +1,479 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Builder.php,v 1.31 2007/01/10 05:32:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + * + * TODO: log output parameters in PECL command line + * TODO: msdev path in configuration + */ + +/** + * Needed for extending PEAR_Builder + */ +require_once 'PEAR/Common.php'; +require_once 'PEAR/PackageFile.php'; +/** + * Class to handle building (compiling) extensions. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since PHP 4.0.2 + * @see http://pear.php.net/manual/en/core.ppm.pear-builder.php + */ +class PEAR_Builder extends PEAR_Common +{ + // {{{ properties + + var $php_api_version = 0; + var $zend_module_api_no = 0; + var $zend_extension_api_no = 0; + + var $extensions_built = array(); + + /** + * @var string Used for reporting when it is not possible to pass function + * via extra parameter, e.g. log, msdevCallback + */ + var $current_callback = null; + + // used for msdev builds + var $_lastline = null; + var $_firstline = null; + // }}} + // {{{ constructor + + /** + * PEAR_Builder constructor. + * + * @param object $ui user interface object (instance of PEAR_Frontend_*) + * + * @access public + */ + function PEAR_Builder(&$ui) + { + parent::PEAR_Common(); + $this->setFrontendObject($ui); + } + + // }}} + + // {{{ _build_win32() + + /** + * Build an extension from source on windows. + * requires msdev + */ + function _build_win32($descfile, $callback = null) + { + if (is_object($descfile)) { + $pkg = $descfile; + $descfile = $pkg->getPackageFile(); + } else { + $pf = &new PEAR_PackageFile($this->config, $this->debug); + $pkg = &$pf->fromPackageFile($descfile, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pkg)) { + return $pkg; + } + } + $dir = dirname($descfile); + $old_cwd = getcwd(); + + if (!file_exists($dir) || !is_dir($dir) || !chdir($dir)) { + return $this->raiseError("could not chdir to $dir"); + } + // packages that were in a .tar have the packagefile in this directory + $vdir = $pkg->getPackage() . '-' . $pkg->getVersion(); + if (file_exists($dir) && is_dir($vdir)) { + if (chdir($vdir)) { + $dir = getcwd(); + } else { + return $this->raiseError("could not chdir to " . realpath($vdir)); + } + } + + $this->log(2, "building in $dir"); + + $dsp = $pkg->getPackage().'.dsp'; + if (!file_exists("$dir/$dsp")) { + return $this->raiseError("The DSP $dsp does not exist."); + } + // XXX TODO: make release build type configurable + $command = 'msdev '.$dsp.' /MAKE "'.$pkg->getPackage(). ' - Release"'; + + $err = $this->_runCommand($command, array(&$this, 'msdevCallback')); + if (PEAR::isError($err)) { + return $err; + } + + // figure out the build platform and type + $platform = 'Win32'; + $buildtype = 'Release'; + if (preg_match('/.*?'.$pkg->getPackage().'\s-\s(\w+)\s(.*?)-+/i',$this->_firstline,$matches)) { + $platform = $matches[1]; + $buildtype = $matches[2]; + } + + if (preg_match('/(.*)?\s-\s(\d+).*?(\d+)/',$this->_lastline,$matches)) { + if ($matches[2]) { + // there were errors in the build + return $this->raiseError("There were errors during compilation."); + } + $out = $matches[1]; + } else { + return $this->raiseError("Did not understand the completion status returned from msdev.exe."); + } + + // msdev doesn't tell us the output directory :/ + // open the dsp, find /out and use that directory + $dsptext = join(file($dsp),''); + + // this regex depends on the build platform and type having been + // correctly identified above. + $regex ='/.*?!IF\s+"\$\(CFG\)"\s+==\s+("'. + $pkg->getPackage().'\s-\s'. + $platform.'\s'. + $buildtype.'").*?'. + '\/out:"(.*?)"/is'; + + if ($dsptext && preg_match($regex,$dsptext,$matches)) { + // what we get back is a relative path to the output file itself. + $outfile = realpath($matches[2]); + } else { + return $this->raiseError("Could not retrieve output information from $dsp."); + } + // realpath returns false if the file doesn't exist + if ($outfile && copy($outfile, "$dir/$out")) { + $outfile = "$dir/$out"; + } + + $built_files[] = array( + 'file' => "$outfile", + 'php_api' => $this->php_api_version, + 'zend_mod_api' => $this->zend_module_api_no, + 'zend_ext_api' => $this->zend_extension_api_no, + ); + + return $built_files; + } + // }}} + + // {{{ msdevCallback() + function msdevCallback($what, $data) + { + if (!$this->_firstline) + $this->_firstline = $data; + $this->_lastline = $data; + call_user_func($this->current_callback, $what, $data); + } + // }}} + + // {{{ _harventInstDir + /** + * @param string + * @param string + * @param array + * @access private + */ + function _harvestInstDir($dest_prefix, $dirname, &$built_files) + { + $d = opendir($dirname); + if (!$d) + return false; + + $ret = true; + while (($ent = readdir($d)) !== false) { + if ($ent{0} == '.') + continue; + + $full = $dirname . DIRECTORY_SEPARATOR . $ent; + if (is_dir($full)) { + if (!$this->_harvestInstDir( + $dest_prefix . DIRECTORY_SEPARATOR . $ent, + $full, $built_files)) { + $ret = false; + break; + } + } else { + $dest = $dest_prefix . DIRECTORY_SEPARATOR . $ent; + $built_files[] = array( + 'file' => $full, + 'dest' => $dest, + 'php_api' => $this->php_api_version, + 'zend_mod_api' => $this->zend_module_api_no, + 'zend_ext_api' => $this->zend_extension_api_no, + ); + } + } + closedir($d); + return $ret; + } + + // }}} + + // {{{ build() + + /** + * Build an extension from source. Runs "phpize" in the source + * directory, but compiles in a temporary directory + * (/var/tmp/pear-build-USER/PACKAGE-VERSION). + * + * @param string|PEAR_PackageFile_v* $descfile path to XML package description file, or + * a PEAR_PackageFile object + * + * @param mixed $callback callback function used to report output, + * see PEAR_Builder::_runCommand for details + * + * @return array an array of associative arrays with built files, + * format: + * array( array( 'file' => '/path/to/ext.so', + * 'php_api' => YYYYMMDD, + * 'zend_mod_api' => YYYYMMDD, + * 'zend_ext_api' => YYYYMMDD ), + * ... ) + * + * @access public + * + * @see PEAR_Builder::_runCommand + */ + function build($descfile, $callback = null) + { + $this->current_callback = $callback; + if (PEAR_OS == "Windows") { + return $this->_build_win32($descfile,$callback); + } + if (PEAR_OS != 'Unix') { + return $this->raiseError("building extensions not supported on this platform"); + } + if (is_object($descfile)) { + $pkg = $descfile; + $descfile = $pkg->getPackageFile(); + } else { + $pf = &new PEAR_PackageFile($this->config); + $pkg = &$pf->fromPackageFile($descfile, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pkg)) { + return $pkg; + } + } + $dir = dirname($descfile); + $old_cwd = getcwd(); + if (!file_exists($dir) || !is_dir($dir) || !chdir($dir)) { + return $this->raiseError("could not chdir to $dir"); + } + $vdir = $pkg->getPackage() . '-' . $pkg->getVersion(); + if (is_dir($vdir)) { + chdir($vdir); + } + $dir = getcwd(); + $this->log(2, "building in $dir"); + putenv('PATH=' . $this->config->get('bin_dir') . ':' . getenv('PATH')); + $err = $this->_runCommand("phpize", array(&$this, 'phpizeCallback')); + if (PEAR::isError($err)) { + return $err; + } + if (!$err) { + return $this->raiseError("`phpize' failed"); + } + + // {{{ start of interactive part + $configure_command = "$dir/configure"; + $configure_options = $pkg->getConfigureOptions(); + if ($configure_options) { + foreach ($configure_options as $o) { + $default = array_key_exists('default', $o) ? $o['default'] : null; + list($r) = $this->ui->userDialog('build', + array($o['prompt']), + array('text'), + array($default)); + if (substr($o['name'], 0, 5) == 'with-' && + ($r == 'yes' || $r == 'autodetect')) { + $configure_command .= " --$o[name]"; + } else { + $configure_command .= " --$o[name]=".trim($r); + } + } + } + // }}} end of interactive part + + // FIXME make configurable + if(!$user=getenv('USER')){ + $user='defaultuser'; + } + $build_basedir = "/var/tmp/pear-build-$user"; + $build_dir = "$build_basedir/$vdir"; + $inst_dir = "$build_basedir/install-$vdir"; + $this->log(1, "building in $build_dir"); + if (is_dir($build_dir)) { + System::rm(array('-rf', $build_dir)); + } + if (!System::mkDir(array('-p', $build_dir))) { + return $this->raiseError("could not create build dir: $build_dir"); + } + $this->addTempFile($build_dir); + if (!System::mkDir(array('-p', $inst_dir))) { + return $this->raiseError("could not create temporary install dir: $inst_dir"); + } + $this->addTempFile($inst_dir); + + if (getenv('MAKE')) { + $make_command = getenv('MAKE'); + } else { + $make_command = 'make'; + } + $to_run = array( + $configure_command, + $make_command, + "$make_command INSTALL_ROOT=\"$inst_dir\" install", + "find \"$inst_dir\" -ls" + ); + if (!file_exists($build_dir) || !is_dir($build_dir) || !chdir($build_dir)) { + return $this->raiseError("could not chdir to $build_dir"); + } + putenv('PHP_PEAR_VERSION=1.6.1'); + foreach ($to_run as $cmd) { + $err = $this->_runCommand($cmd, $callback); + if (PEAR::isError($err)) { + chdir($old_cwd); + return $err; + } + if (!$err) { + chdir($old_cwd); + return $this->raiseError("`$cmd' failed"); + } + } + if (!($dp = opendir("modules"))) { + chdir($old_cwd); + return $this->raiseError("no `modules' directory found"); + } + $built_files = array(); + $prefix = exec("php-config --prefix"); + $this->_harvestInstDir($prefix, $inst_dir . DIRECTORY_SEPARATOR . $prefix, $built_files); + chdir($old_cwd); + return $built_files; + } + + // }}} + // {{{ phpizeCallback() + + /** + * Message callback function used when running the "phpize" + * program. Extracts the API numbers used. Ignores other message + * types than "cmdoutput". + * + * @param string $what the type of message + * @param mixed $data the message + * + * @return void + * + * @access public + */ + function phpizeCallback($what, $data) + { + if ($what != 'cmdoutput') { + return; + } + $this->log(1, rtrim($data)); + if (preg_match('/You should update your .aclocal.m4/', $data)) { + return; + } + $matches = array(); + if (preg_match('/^\s+(\S[^:]+):\s+(\d{8})/', $data, $matches)) { + $member = preg_replace('/[^a-z]/', '_', strtolower($matches[1])); + $apino = (int)$matches[2]; + if (isset($this->$member)) { + $this->$member = $apino; + //$msg = sprintf("%-22s : %d", $matches[1], $apino); + //$this->log(1, $msg); + } + } + } + + // }}} + // {{{ _runCommand() + + /** + * Run an external command, using a message callback to report + * output. The command will be run through popen and output is + * reported for every line with a "cmdoutput" message with the + * line string, including newlines, as payload. + * + * @param string $command the command to run + * + * @param mixed $callback (optional) function to use as message + * callback + * + * @return bool whether the command was successful (exit code 0 + * means success, any other means failure) + * + * @access private + */ + function _runCommand($command, $callback = null) + { + $this->log(1, "running: $command"); + $pp = popen("$command 2>&1", "r"); + if (!$pp) { + return $this->raiseError("failed to run `$command'"); + } + if ($callback && $callback[0]->debug == 1) { + $olddbg = $callback[0]->debug; + $callback[0]->debug = 2; + } + + while ($line = fgets($pp, 1024)) { + if ($callback) { + call_user_func($callback, 'cmdoutput', $line); + } else { + $this->log(2, rtrim($line)); + } + } + if ($callback && isset($olddbg)) { + $callback[0]->debug = $olddbg; + } + if (is_resource($pp)) { + $exitcode = pclose($pp); + } else { + $exitcode = -1; + } + return ($exitcode == 0); + } + + // }}} + // {{{ log() + + function log($level, $msg) + { + if ($this->current_callback) { + if ($this->debug >= $level) { + call_user_func($this->current_callback, 'output', $msg); + } + return; + } + return PEAR_Common::log($level, $msg); + } + + // }}} +} + +?> diff --git a/PEAR/ChannelFile.php b/PEAR/ChannelFile.php new file mode 100644 index 0000000..5c6ff57 --- /dev/null +++ b/PEAR/ChannelFile.php @@ -0,0 +1,1615 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ChannelFile.php,v 1.79 2007/05/19 23:46:06 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Needed for error handling + */ +require_once 'PEAR/ErrorStack.php'; +require_once 'PEAR/XMLParser.php'; +require_once 'PEAR/Common.php'; + +/** + * Error code if the channel.xml tag does not contain a valid version + */ +define('PEAR_CHANNELFILE_ERROR_NO_VERSION', 1); +/** + * Error code if the channel.xml tag version is not supported (version 1.0 is the only supported version, + * currently + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_VERSION', 2); + +/** + * Error code if parsing is attempted with no xml extension + */ +define('PEAR_CHANNELFILE_ERROR_NO_XML_EXT', 3); + +/** + * Error code if creating the xml parser resource fails + */ +define('PEAR_CHANNELFILE_ERROR_CANT_MAKE_PARSER', 4); + +/** + * Error code used for all sax xml parsing errors + */ +define('PEAR_CHANNELFILE_ERROR_PARSER_ERROR', 5); + +/**#@+ + * Validation errors + */ +/** + * Error code when channel name is missing + */ +define('PEAR_CHANNELFILE_ERROR_NO_NAME', 6); +/** + * Error code when channel name is invalid + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_NAME', 7); +/** + * Error code when channel summary is missing + */ +define('PEAR_CHANNELFILE_ERROR_NO_SUMMARY', 8); +/** + * Error code when channel summary is multi-line + */ +define('PEAR_CHANNELFILE_ERROR_MULTILINE_SUMMARY', 9); +/** + * Error code when channel server is missing for xmlrpc or soap protocol + */ +define('PEAR_CHANNELFILE_ERROR_NO_HOST', 10); +/** + * Error code when channel server is invalid for xmlrpc or soap protocol + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_HOST', 11); +/** + * Error code when a mirror name is invalid + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_MIRROR', 21); +/** + * Error code when a mirror type is invalid + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_MIRRORTYPE', 22); +/** + * Error code when an attempt is made to generate xml, but the parsed content is invalid + */ +define('PEAR_CHANNELFILE_ERROR_INVALID', 23); +/** + * Error code when an empty package name validate regex is passed in + */ +define('PEAR_CHANNELFILE_ERROR_EMPTY_REGEX', 24); +/** + * Error code when a tag has no version + */ +define('PEAR_CHANNELFILE_ERROR_NO_FUNCTIONVERSION', 25); +/** + * Error code when a tag has no name + */ +define('PEAR_CHANNELFILE_ERROR_NO_FUNCTIONNAME', 26); +/** + * Error code when a tag has no name + */ +define('PEAR_CHANNELFILE_ERROR_NOVALIDATE_NAME', 27); +/** + * Error code when a tag has no version attribute + */ +define('PEAR_CHANNELFILE_ERROR_NOVALIDATE_VERSION', 28); +/** + * Error code when a mirror does not exist but is called for in one of the set* + * methods. + */ +define('PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND', 32); +/** + * Error code when a server port is not numeric + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_PORT', 33); +/** + * Error code when contains no version attribute + */ +define('PEAR_CHANNELFILE_ERROR_NO_STATICVERSION', 34); +/** + * Error code when contains no type attribute in a protocol definition + */ +define('PEAR_CHANNELFILE_ERROR_NOBASEURLTYPE', 35); +/** + * Error code when a mirror is defined and the channel.xml represents the __uri pseudo-channel + */ +define('PEAR_CHANNELFILE_URI_CANT_MIRROR', 36); +/** + * Error code when ssl attribute is present and is not "yes" + */ +define('PEAR_CHANNELFILE_ERROR_INVALID_SSL', 37); +/**#@-*/ + +/** + * Mirror types allowed. Currently only internet servers are recognized. + */ +$GLOBALS['_PEAR_CHANNELS_MIRROR_TYPES'] = array('server'); + + +/** + * The Channel handling class + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_ChannelFile { + /** + * @access private + * @var PEAR_ErrorStack + * @access private + */ + var $_stack; + + /** + * Supported channel.xml versions, for parsing + * @var array + * @access private + */ + var $_supportedVersions = array('1.0'); + + /** + * Parsed channel information + * @var array + * @access private + */ + var $_channelInfo; + + /** + * index into the subchannels array, used for parsing xml + * @var int + * @access private + */ + var $_subchannelIndex; + + /** + * index into the mirrors array, used for parsing xml + * @var int + * @access private + */ + var $_mirrorIndex; + + /** + * Flag used to determine the validity of parsed content + * @var boolean + * @access private + */ + var $_isValid = false; + + function PEAR_ChannelFile() + { + $this->_stack = &new PEAR_ErrorStack('PEAR_ChannelFile'); + $this->_stack->setErrorMessageTemplate($this->_getErrorMessage()); + $this->_isValid = false; + } + + /** + * @return array + * @access protected + */ + function _getErrorMessage() + { + return + array( + PEAR_CHANNELFILE_ERROR_INVALID_VERSION => + 'While parsing channel.xml, an invalid version number "%version% was passed in, expecting one of %versions%', + PEAR_CHANNELFILE_ERROR_NO_VERSION => + 'No version number found in tag', + PEAR_CHANNELFILE_ERROR_NO_XML_EXT => + '%error%', + PEAR_CHANNELFILE_ERROR_CANT_MAKE_PARSER => + 'Unable to create XML parser', + PEAR_CHANNELFILE_ERROR_PARSER_ERROR => + '%error%', + PEAR_CHANNELFILE_ERROR_NO_NAME => + 'Missing channel name', + PEAR_CHANNELFILE_ERROR_INVALID_NAME => + 'Invalid channel %tag% "%name%"', + PEAR_CHANNELFILE_ERROR_NO_SUMMARY => + 'Missing channel summary', + PEAR_CHANNELFILE_ERROR_MULTILINE_SUMMARY => + 'Channel summary should be on one line, but is multi-line', + PEAR_CHANNELFILE_ERROR_NO_HOST => + 'Missing channel server for %type% server', + PEAR_CHANNELFILE_ERROR_INVALID_HOST => + 'Server name "%server%" is invalid for %type% server', + PEAR_CHANNELFILE_ERROR_INVALID_MIRROR => + 'Invalid mirror name "%name%", mirror type %type%', + PEAR_CHANNELFILE_ERROR_INVALID_MIRRORTYPE => + 'Invalid mirror type "%type%"', + PEAR_CHANNELFILE_ERROR_INVALID => + 'Cannot generate xml, contents are invalid', + PEAR_CHANNELFILE_ERROR_EMPTY_REGEX => + 'packagenameregex cannot be empty', + PEAR_CHANNELFILE_ERROR_NO_FUNCTIONVERSION => + '%parent% %protocol% function has no version', + PEAR_CHANNELFILE_ERROR_NO_FUNCTIONNAME => + '%parent% %protocol% function has no name', + PEAR_CHANNELFILE_ERROR_NOBASEURLTYPE => + '%parent% rest baseurl has no type', + PEAR_CHANNELFILE_ERROR_NOVALIDATE_NAME => + 'Validation package has no name in tag', + PEAR_CHANNELFILE_ERROR_NOVALIDATE_VERSION => + 'Validation package "%package%" has no version', + PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND => + 'Mirror "%mirror%" does not exist', + PEAR_CHANNELFILE_ERROR_INVALID_PORT => + 'Port "%port%" must be numeric', + PEAR_CHANNELFILE_ERROR_NO_STATICVERSION => + ' tag must contain version attribute', + PEAR_CHANNELFILE_URI_CANT_MIRROR => + 'The __uri pseudo-channel cannot have mirrors', + PEAR_CHANNELFILE_ERROR_INVALID_SSL => + '%server% has invalid ssl attribute "%ssl%" can only be yes or not present', + ); + } + + /** + * @param string contents of package.xml file + * @return bool success of parsing + */ + function fromXmlString($data) + { + if (preg_match('/_supportedVersions)) { + $this->_stack->push(PEAR_CHANNELFILE_ERROR_INVALID_VERSION, 'error', + array('version' => $channelversion[1])); + return false; + } + $parser = new PEAR_XMLParser; + $result = $parser->parse($data); + if ($result !== true) { + if ($result->getCode() == 1) { + $this->_stack->push(PEAR_CHANNELFILE_ERROR_NO_XML_EXT, 'error', + array('error' => $result->getMessage())); + } else { + $this->_stack->push(PEAR_CHANNELFILE_ERROR_CANT_MAKE_PARSER, 'error'); + } + return false; + } + $this->_channelInfo = $parser->getData(); + return true; + } else { + $this->_stack->push(PEAR_CHANNELFILE_ERROR_NO_VERSION, 'error', array('xml' => $data)); + return false; + } + } + + /** + * @return array + */ + function toArray() + { + if (!$this->_isValid && !$this->validate()) { + return false; + } + return $this->_channelInfo; + } + + /** + * @param array + * @static + * @return PEAR_ChannelFile|false false if invalid + */ + function &fromArray($data, $compatibility = false, $stackClass = 'PEAR_ErrorStack') + { + $a = new PEAR_ChannelFile($compatibility, $stackClass); + $a->_fromArray($data); + if (!$a->validate()) { + $a = false; + return $a; + } + return $a; + } + + /** + * Unlike {@link fromArray()} this does not do any validation + * @param array + * @static + * @return PEAR_ChannelFile + */ + function &fromArrayWithErrors($data, $compatibility = false, + $stackClass = 'PEAR_ErrorStack') + { + $a = new PEAR_ChannelFile($compatibility, $stackClass); + $a->_fromArray($data); + return $a; + } + + /** + * @param array + * @access private + */ + function _fromArray($data) + { + $this->_channelInfo = $data; + } + + /** + * Wrapper to {@link PEAR_ErrorStack::getErrors()} + * @param boolean determines whether to purge the error stack after retrieving + * @return array + */ + function getErrors($purge = false) + { + return $this->_stack->getErrors($purge); + } + + /** + * Unindent given string (?) + * + * @param string $str The string that has to be unindented. + * @return string + * @access private + */ + function _unIndent($str) + { + // remove leading newlines + $str = preg_replace('/^[\r\n]+/', '', $str); + // find whitespace at the beginning of the first line + $indent_len = strspn($str, " \t"); + $indent = substr($str, 0, $indent_len); + $data = ''; + // remove the same amount of whitespace from following lines + foreach (explode("\n", $str) as $line) { + if (substr($line, 0, $indent_len) == $indent) { + $data .= substr($line, $indent_len) . "\n"; + } + } + return $data; + } + + /** + * Parse a channel.xml file. Expects the name of + * a channel xml file as input. + * + * @param string $descfile name of channel xml file + * @return bool success of parsing + */ + function fromXmlFile($descfile) + { + if (!file_exists($descfile) || !is_file($descfile) || !is_readable($descfile) || + (!$fp = fopen($descfile, 'r'))) { + require_once 'PEAR.php'; + return PEAR::raiseError("Unable to open $descfile"); + } + + // read the whole thing so we only get one cdata callback + // for each block of cdata + fclose($fp); + $data = file_get_contents($descfile); + return $this->fromXmlString($data); + } + + /** + * Parse channel information from different sources + * + * This method is able to extract information about a channel + * from an .xml file or a string + * + * @access public + * @param string Filename of the source or the source itself + * @return bool + */ + function fromAny($info) + { + if (is_string($info) && file_exists($info) && strlen($info) < 255) { + $tmp = substr($info, -4); + if ($tmp == '.xml') { + $info = $this->fromXmlFile($info); + } else { + $fp = fopen($info, "r"); + $test = fread($fp, 5); + fclose($fp); + if ($test == "fromXmlFile($info); + } + } + if (PEAR::isError($info)) { + require_once 'PEAR.php'; + return PEAR::raiseError($info); + } + } + if (is_string($info)) { + $info = $this->fromXmlString($info); + } + return $info; + } + + /** + * Return an XML document based on previous parsing and modifications + * + * @return string XML data + * + * @access public + */ + function toXml() + { + if (!$this->_isValid && !$this->validate()) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID); + return false; + } + if (!isset($this->_channelInfo['attribs']['version'])) { + $this->_channelInfo['attribs']['version'] = '1.0'; + } + $channelInfo = $this->_channelInfo; + $ret = "\n"; + $ret .= " + $channelInfo[name] + " . htmlspecialchars($channelInfo['summary'])." +"; + if (isset($channelInfo['suggestedalias'])) { + $ret .= ' ' . $channelInfo['suggestedalias'] . "\n"; + } + if (isset($channelInfo['validatepackage'])) { + $ret .= ' ' . + htmlspecialchars($channelInfo['validatepackage']['_content']) . + "\n"; + } + $ret .= " \n"; + $ret .= ' _makeXmlrpcXml($channelInfo['servers']['primary']['xmlrpc'], ' '); + } + if (isset($channelInfo['servers']['primary']['rest'])) { + $ret .= $this->_makeRestXml($channelInfo['servers']['primary']['rest'], ' '); + } + if (isset($channelInfo['servers']['primary']['soap'])) { + $ret .= $this->_makeSoapXml($channelInfo['servers']['primary']['soap'], ' '); + } + $ret .= " \n"; + if (isset($channelInfo['servers']['mirror'])) { + $ret .= $this->_makeMirrorsXml($channelInfo); + } + $ret .= " \n"; + $ret .= ""; + return str_replace("\r", "\n", str_replace("\r\n", "\n", $ret)); + } + + /** + * Generate the tag + * @access private + */ + function _makeXmlrpcXml($info, $indent) + { + $ret = $indent . "_makeFunctionsXml($info['function'], "$indent "); + $ret .= $indent . "\n"; + return $ret; + } + + /** + * Generate the tag + * @access private + */ + function _makeSoapXml($info, $indent) + { + $ret = $indent . "_makeFunctionsXml($info['function'], "$indent "); + $ret .= $indent . "\n"; + return $ret; + } + + /** + * Generate the tag + * @access private + */ + function _makeRestXml($info, $indent) + { + $ret = $indent . "\n"; + if (!isset($info['baseurl'][0])) { + $info['baseurl'] = array($info['baseurl']); + } + foreach ($info['baseurl'] as $url) { + $ret .= "$indent \n"; + } + $ret .= $indent . "\n"; + return $ret; + } + + /** + * Generate the tag + * @access private + */ + function _makeMirrorsXml($channelInfo) + { + $ret = ""; + if (!isset($channelInfo['servers']['mirror'][0])) { + $channelInfo['servers']['mirror'] = array($channelInfo['servers']['mirror']); + } + foreach ($channelInfo['servers']['mirror'] as $mirror) { + $ret .= ' _makeXmlrpcXml($mirror['xmlrpc'], ' '); + } + if (isset($mirror['rest'])) { + $ret .= $this->_makeRestXml($mirror['rest'], ' '); + } + if (isset($mirror['soap'])) { + $ret .= $this->_makeSoapXml($mirror['soap'], ' '); + } + $ret .= " \n"; + } else { + $ret .= "/>\n"; + } + } + return $ret; + } + + /** + * Generate the tag + * @access private + */ + function _makeFunctionsXml($functions, $indent, $rest = false) + { + $ret = ''; + if (!isset($functions[0])) { + $functions = array($functions); + } + foreach ($functions as $function) { + $ret .= "$indent\n"; + } + return $ret; + } + + /** + * Validation error. Also marks the object contents as invalid + * @param error code + * @param array error information + * @access private + */ + function _validateError($code, $params = array()) + { + $this->_stack->push($code, 'error', $params); + $this->_isValid = false; + } + + /** + * Validation warning. Does not mark the object contents invalid. + * @param error code + * @param array error information + * @access private + */ + function _validateWarning($code, $params = array()) + { + $this->_stack->push($code, 'warning', $params); + } + + /** + * Validate parsed file. + * + * @access public + * @return boolean + */ + function validate() + { + $this->_isValid = true; + $info = $this->_channelInfo; + if (empty($info['name'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_NAME); + } elseif (!$this->validChannelServer($info['name'])) { + if ($info['name'] != '__uri') { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_NAME, array('tag' => 'name', + 'name' => $info['name'])); + } + } + if (empty($info['summary'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_SUMMARY); + } elseif (strpos(trim($info['summary']), "\n") !== false) { + $this->_validateWarning(PEAR_CHANNELFILE_ERROR_MULTILINE_SUMMARY, + array('summary' => $info['summary'])); + } + if (isset($info['suggestedalias'])) { + if (!$this->validChannelServer($info['suggestedalias'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_NAME, + array('tag' => 'suggestedalias', 'name' =>$info['suggestedalias'])); + } + } + if (isset($info['localalias'])) { + if (!$this->validChannelServer($info['localalias'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_NAME, + array('tag' => 'localalias', 'name' =>$info['localalias'])); + } + } + if (isset($info['validatepackage'])) { + if (!isset($info['validatepackage']['_content'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NOVALIDATE_NAME); + } + if (!isset($info['validatepackage']['attribs']['version'])) { + $content = isset($info['validatepackage']['_content']) ? + $info['validatepackage']['_content'] : + null; + $this->_validateError(PEAR_CHANNELFILE_ERROR_NOVALIDATE_VERSION, + array('package' => $content)); + } + } + if (isset($info['servers']['primary']['attribs']['port']) && + !is_numeric($info['servers']['primary']['attribs']['port'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_PORT, + array('port' => $info['servers']['primary']['attribs']['port'])); + } + if (isset($info['servers']['primary']['attribs']['ssl']) && + $info['servers']['primary']['attribs']['ssl'] != 'yes') { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_SSL, + array('ssl' => $info['servers']['primary']['attribs']['ssl'], + 'server' => $info['name'])); + } + + if (isset($info['servers']['primary']['xmlrpc']) && + isset($info['servers']['primary']['xmlrpc']['function'])) { + $this->_validateFunctions('xmlrpc', $info['servers']['primary']['xmlrpc']['function']); + } + if (isset($info['servers']['primary']['soap']) && + isset($info['servers']['primary']['soap']['function'])) { + $this->_validateFunctions('soap', $info['servers']['primary']['soap']['function']); + } + if (isset($info['servers']['primary']['rest']) && + isset($info['servers']['primary']['rest']['baseurl'])) { + $this->_validateFunctions('rest', $info['servers']['primary']['rest']['baseurl']); + } + if (isset($info['servers']['mirror'])) { + if ($this->_channelInfo['name'] == '__uri') { + $this->_validateError(PEAR_CHANNELFILE_URI_CANT_MIRROR); + } + if (!isset($info['servers']['mirror'][0])) { + $info['servers']['mirror'] = array($info['servers']['mirror']); + } + foreach ($info['servers']['mirror'] as $mirror) { + if (!isset($mirror['attribs']['host'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_HOST, + array('type' => 'mirror')); + } elseif (!$this->validChannelServer($mirror['attribs']['host'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_HOST, + array('server' => $mirror['attribs']['host'], 'type' => 'mirror')); + } + if (isset($mirror['attribs']['ssl']) && $mirror['attribs']['ssl'] != 'yes') { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_SSL, + array('ssl' => $info['ssl'], 'server' => $mirror['attribs']['host'])); + } + if (isset($mirror['xmlrpc'])) { + $this->_validateFunctions('xmlrpc', + $mirror['xmlrpc']['function'], $mirror['attribs']['host']); + } + if (isset($mirror['soap'])) { + $this->_validateFunctions('soap', $mirror['soap']['function'], + $mirror['attribs']['host']); + } + if (isset($mirror['rest'])) { + $this->_validateFunctions('rest', $mirror['rest']['baseurl'], + $mirror['attribs']['host']); + } + } + } + return $this->_isValid; + } + + /** + * @param string xmlrpc or soap - protocol name this function applies to + * @param array the functions + * @param string the name of the parent element (mirror name, for instance) + */ + function _validateFunctions($protocol, $functions, $parent = '') + { + if (!isset($functions[0])) { + $functions = array($functions); + } + foreach ($functions as $function) { + if (!isset($function['_content']) || empty($function['_content'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_FUNCTIONNAME, + array('parent' => $parent, 'protocol' => $protocol)); + } + if ($protocol == 'rest') { + if (!isset($function['attribs']['type']) || + empty($function['attribs']['type'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_BASEURLTYPE, + array('parent' => $parent, 'protocol' => $protocol)); + } + } else { + if (!isset($function['attribs']['version']) || + empty($function['attribs']['version'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_FUNCTIONVERSION, + array('parent' => $parent, 'protocol' => $protocol)); + } + } + } + } + + /** + * Test whether a string contains a valid channel server. + * @param string $ver the package version to test + * @return bool + */ + function validChannelServer($server) + { + if ($server == '__uri') { + return true; + } + return (bool) preg_match(PEAR_CHANNELS_SERVER_PREG, $server); + } + + /** + * @return string|false + */ + function getName() + { + if (isset($this->_channelInfo['name'])) { + return $this->_channelInfo['name']; + } else { + return false; + } + } + + /** + * @return string|false + */ + function getServer() + { + if (isset($this->_channelInfo['name'])) { + return $this->_channelInfo['name']; + } else { + return false; + } + } + + /** + * @return int|80 port number to connect to + */ + function getPort($mirror = false) + { + if ($mirror) { + if ($mir = $this->getMirror($mirror)) { + if (isset($mir['attribs']['port'])) { + return $mir['attribs']['port']; + } else { + if ($this->getSSL($mirror)) { + return 443; + } + return 80; + } + } + return false; + } + if (isset($this->_channelInfo['servers']['primary']['attribs']['port'])) { + return $this->_channelInfo['servers']['primary']['attribs']['port']; + } + if ($this->getSSL()) { + return 443; + } + return 80; + } + + /** + * @return bool Determines whether secure sockets layer (SSL) is used to connect to this channel + */ + function getSSL($mirror = false) + { + if ($mirror) { + if ($mir = $this->getMirror($mirror)) { + if (isset($mir['attribs']['ssl'])) { + return true; + } else { + return false; + } + } + return false; + } + if (isset($this->_channelInfo['servers']['primary']['attribs']['ssl'])) { + return true; + } + return false; + } + + /** + * @return string|false + */ + function getSummary() + { + if (isset($this->_channelInfo['summary'])) { + return $this->_channelInfo['summary']; + } else { + return false; + } + } + + /** + * @param string xmlrpc or soap + * @param string|false mirror name or false for primary server + */ + function getPath($protocol, $mirror = false) + { + if (!in_array($protocol, array('xmlrpc', 'soap'))) { + return false; + } + if ($mirror) { + if (!($mir = $this->getMirror($mirror))) { + return false; + } + if (isset($mir[$protocol]['attribs']['path'])) { + return $mir[$protocol]['attribs']['path']; + } else { + return $protocol . '.php'; + } + } elseif (isset($this->_channelInfo['servers']['primary'][$protocol]['attribs']['path'])) { + return $this->_channelInfo['servers']['primary'][$protocol]['attribs']['path']; + } + return $protocol . '.php'; + } + + /** + * @param string protocol type (xmlrpc, soap) + * @param string Mirror name + * @return array|false + */ + function getFunctions($protocol, $mirror = false) + { + if ($this->getName() == '__uri') { + return false; + } + if ($protocol == 'rest') { + $function = 'baseurl'; + } else { + $function = 'function'; + } + if ($mirror) { + if ($mir = $this->getMirror($mirror)) { + if (isset($mir[$protocol][$function])) { + return $mir[$protocol][$function]; + } + } + return false; + } + if (isset($this->_channelInfo['servers']['primary'][$protocol][$function])) { + return $this->_channelInfo['servers']['primary'][$protocol][$function]; + } else { + return false; + } + } + + /** + * @param string Protocol type + * @param string Function name (null to return the + * first protocol of the type requested) + * @param string Mirror name, if any + * @return array + */ + function getFunction($type, $name = null, $mirror = false) + { + $protocols = $this->getFunctions($type, $mirror); + if (!$protocols) { + return false; + } + foreach ($protocols as $protocol) { + if ($name === null) { + return $protocol; + } + if ($protocol['_content'] != $name) { + continue; + } + return $protocol; + } + return false; + } + + /** + * @param string protocol type + * @param string protocol name + * @param string version + * @param string mirror name + * @return boolean + */ + function supports($type, $name = null, $mirror = false, $version = '1.0') + { + $protocols = $this->getFunctions($type, $mirror); + if (!$protocols) { + return false; + } + foreach ($protocols as $protocol) { + if ($protocol['attribs']['version'] != $version) { + continue; + } + if ($name === null) { + return true; + } + if ($protocol['_content'] != $name) { + continue; + } + return true; + } + return false; + } + + /** + * Determines whether a channel supports Representational State Transfer (REST) protocols + * for retrieving channel information + * @param string + * @return bool + */ + function supportsREST($mirror = false) + { + if ($mirror == $this->_channelInfo['name']) { + $mirror = false; + } + if ($mirror) { + if ($mir = $this->getMirror($mirror)) { + return isset($mir['rest']); + } + return false; + } + return isset($this->_channelInfo['servers']['primary']['rest']); + } + + /** + * Get the URL to access a base resource. + * + * Hyperlinks in the returned xml will be used to retrieve the proper information + * needed. This allows extreme extensibility and flexibility in implementation + * @param string Resource Type to retrieve + */ + function getBaseURL($resourceType, $mirror = false) + { + if ($mirror == $this->_channelInfo['name']) { + $mirror = false; + } + if ($mirror) { + if ($mir = $this->getMirror($mirror)) { + $rest = $mir['rest']; + } else { + return false; + } + } else { + $rest = $this->_channelInfo['servers']['primary']['rest']; + } + if (!isset($rest['baseurl'][0])) { + $rest['baseurl'] = array($rest['baseurl']); + } + foreach ($rest['baseurl'] as $baseurl) { + if (strtolower($baseurl['attribs']['type']) == strtolower($resourceType)) { + return $baseurl['_content']; + } + } + return false; + } + + /** + * Since REST does not implement RPC, provide this as a logical wrapper around + * resetFunctions for REST + * @param string|false mirror name, if any + */ + function resetREST($mirror = false) + { + return $this->resetFunctions('rest', $mirror); + } + + /** + * Empty all protocol definitions + * @param string protocol type (xmlrpc, soap) + * @param string|false mirror name, if any + */ + function resetFunctions($type, $mirror = false) + { + if ($mirror) { + if (isset($this->_channelInfo['servers']['mirror'])) { + $mirrors = $this->_channelInfo['servers']['mirror']; + if (!isset($mirrors[0])) { + $mirrors = array($mirrors); + } + foreach ($mirrors as $i => $mir) { + if ($mir['attribs']['host'] == $mirror) { + if (isset($this->_channelInfo['servers']['mirror'][$i][$type])) { + unset($this->_channelInfo['servers']['mirror'][$i][$type]); + } + return true; + } + } + return false; + } else { + return false; + } + } else { + if (isset($this->_channelInfo['servers']['primary'][$type])) { + unset($this->_channelInfo['servers']['primary'][$type]); + } + return true; + } + } + + /** + * Set a channel's protocols to the protocols supported by pearweb + */ + function setDefaultPEARProtocols($version = '1.0', $mirror = false) + { + switch ($version) { + case '1.0' : + $this->resetFunctions('xmlrpc', $mirror); + $this->resetFunctions('soap', $mirror); + $this->resetREST($mirror); + $this->addFunction('xmlrpc', '1.0', 'logintest', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.listLatestReleases', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.listAll', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.info', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.getDownloadURL', $mirror); + $this->addFunction('xmlrpc', '1.1', 'package.getDownloadURL', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.getDepDownloadURL', $mirror); + $this->addFunction('xmlrpc', '1.1', 'package.getDepDownloadURL', $mirror); + $this->addFunction('xmlrpc', '1.0', 'package.search', $mirror); + $this->addFunction('xmlrpc', '1.0', 'channel.listAll', $mirror); + return true; + break; + default : + return false; + break; + } + } + + /** + * @return array + */ + function getMirrors() + { + if (isset($this->_channelInfo['servers']['mirror'])) { + $mirrors = $this->_channelInfo['servers']['mirror']; + if (!isset($mirrors[0])) { + $mirrors = array($mirrors); + } + return $mirrors; + } else { + return array(); + } + } + + /** + * Get the unserialized XML representing a mirror + * @return array|false + */ + function getMirror($server) + { + foreach ($this->getMirrors() as $mirror) { + if ($mirror['attribs']['host'] == $server) { + return $mirror; + } + } + return false; + } + + /** + * @param string + * @return string|false + * @error PEAR_CHANNELFILE_ERROR_NO_NAME + * @error PEAR_CHANNELFILE_ERROR_INVALID_NAME + */ + function setName($name) + { + return $this->setServer($name); + } + + /** + * Set the socket number (port) that is used to connect to this channel + * @param integer + * @param string|false name of the mirror server, or false for the primary + */ + function setPort($port, $mirror = false) + { + if ($mirror) { + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + if (isset($this->_channelInfo['servers']['mirror'][0])) { + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + $this->_channelInfo['servers']['mirror'][$i]['attribs']['port'] = $port; + return true; + } + } + return false; + } elseif ($this->_channelInfo['servers']['mirror']['attribs']['host'] == $mirror) { + $this->_channelInfo['servers']['mirror']['attribs']['port'] = $port; + $this->_isValid = false; + return true; + } + } + $this->_channelInfo['servers']['primary']['attribs']['port'] = $port; + $this->_isValid = false; + return true; + } + + /** + * Set the socket number (port) that is used to connect to this channel + * @param bool Determines whether to turn on SSL support or turn it off + * @param string|false name of the mirror server, or false for the primary + */ + function setSSL($ssl = true, $mirror = false) + { + if ($mirror) { + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + if (isset($this->_channelInfo['servers']['mirror'][0])) { + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + if (!$ssl) { + if (isset($this->_channelInfo['servers']['mirror'][$i] + ['attribs']['ssl'])) { + unset($this->_channelInfo['servers']['mirror'][$i]['attribs']['ssl']); + } + } else { + $this->_channelInfo['servers']['mirror'][$i]['attribs']['ssl'] = 'yes'; + } + return true; + } + } + return false; + } elseif ($this->_channelInfo['servers']['mirror']['attribs']['host'] == $mirror) { + if (!$ssl) { + if (isset($this->_channelInfo['servers']['mirror']['attribs']['ssl'])) { + unset($this->_channelInfo['servers']['mirror']['attribs']['ssl']); + } + } else { + $this->_channelInfo['servers']['mirror']['attribs']['ssl'] = 'yes'; + } + $this->_isValid = false; + return true; + } + } + if ($ssl) { + $this->_channelInfo['servers']['primary']['attribs']['ssl'] = 'yes'; + } else { + if (isset($this->_channelInfo['servers']['primary']['attribs']['ssl'])) { + unset($this->_channelInfo['servers']['primary']['attribs']['ssl']); + } + } + $this->_isValid = false; + return true; + } + + /** + * Set the socket number (port) that is used to connect to this channel + * @param integer + * @param string|false name of the mirror server, or false for the primary + */ + function setPath($protocol, $path, $mirror = false) + { + if (!in_array($protocol, array('xmlrpc', 'soap'))) { + return false; + } + if ($mirror) { + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + if (isset($this->_channelInfo['servers']['mirror'][0])) { + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + $this->_channelInfo['servers']['mirror'][$i][$protocol]['attribs']['path'] = + $path; + return true; + } + } + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } elseif ($this->_channelInfo['servers']['mirror']['attribs']['host'] == $mirror) { + $this->_channelInfo['servers']['mirror'][$protocol]['attribs']['path'] = $path; + $this->_isValid = false; + return true; + } + } + $this->_channelInfo['servers']['primary'][$protocol]['attribs']['path'] = $path; + $this->_isValid = false; + return true; + } + + /** + * @param string + * @return string|false + * @error PEAR_CHANNELFILE_ERROR_NO_SERVER + * @error PEAR_CHANNELFILE_ERROR_INVALID_SERVER + */ + function setServer($server, $mirror = false) + { + if (empty($server)) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_SERVER); + return false; + } elseif (!$this->validChannelServer($server)) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_NAME, + array('tag' => 'name', 'name' => $server)); + return false; + } + if ($mirror) { + $found = false; + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + $found = true; + break; + } + } + if (!$found) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + $this->_channelInfo['mirror'][$i]['attribs']['host'] = $server; + return true; + } + $this->_channelInfo['name'] = $server; + return true; + } + + /** + * @param string + * @return boolean success + * @error PEAR_CHANNELFILE_ERROR_NO_SUMMARY + * @warning PEAR_CHANNELFILE_ERROR_MULTILINE_SUMMARY + */ + function setSummary($summary) + { + if (empty($summary)) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_NO_SUMMARY); + return false; + } elseif (strpos(trim($summary), "\n") !== false) { + $this->_validateWarning(PEAR_CHANNELFILE_ERROR_MULTILINE_SUMMARY, + array('summary' => $summary)); + } + $this->_channelInfo['summary'] = $summary; + return true; + } + + /** + * @param string + * @param boolean determines whether the alias is in channel.xml or local + * @return boolean success + */ + function setAlias($alias, $local = false) + { + if (!$this->validChannelServer($alias)) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_INVALID_NAME, + array('tag' => 'suggestedalias', 'name' => $alias)); + return false; + } + if ($local) { + $this->_channelInfo['localalias'] = $alias; + } else { + $this->_channelInfo['suggestedalias'] = $alias; + } + return true; + } + + /** + * @return string + */ + function getAlias() + { + if (isset($this->_channelInfo['localalias'])) { + return $this->_channelInfo['localalias']; + } + if (isset($this->_channelInfo['suggestedalias'])) { + return $this->_channelInfo['suggestedalias']; + } + if (isset($this->_channelInfo['name'])) { + return $this->_channelInfo['name']; + } + return ''; + } + + /** + * Set the package validation object if it differs from PEAR's default + * The class must be includeable via changing _ in the classname to path separator, + * but no checking of this is made. + * @param string|false pass in false to reset to the default packagename regex + * @return boolean success + */ + function setValidationPackage($validateclass, $version) + { + if (empty($validateclass)) { + unset($this->_channelInfo['validatepackage']); + } + $this->_channelInfo['validatepackage'] = array('_content' => $validateclass); + $this->_channelInfo['validatepackage']['attribs'] = array('version' => $version); + } + + /** + * Add a protocol to the provides section + * @param string protocol type + * @param string protocol version + * @param string protocol name, if any + * @param string mirror name, if this is a mirror's protocol + * @return bool + */ + function addFunction($type, $version, $name = '', $mirror = false) + { + if ($mirror) { + return $this->addMirrorFunction($mirror, $type, $version, $name); + } + $set = array('attribs' => array('version' => $version), '_content' => $name); + if (!isset($this->_channelInfo['servers']['primary'][$type]['function'])) { + if (!isset($this->_channelInfo['servers'])) { + $this->_channelInfo['servers'] = array('primary' => + array($type => array())); + } elseif (!isset($this->_channelInfo['servers']['primary'])) { + $this->_channelInfo['servers']['primary'] = array($type => array()); + } + $this->_channelInfo['servers']['primary'][$type]['function'] = $set; + $this->_isValid = false; + return true; + } elseif (!isset($this->_channelInfo['servers']['primary'][$type]['function'][0])) { + $this->_channelInfo['servers']['primary'][$type]['function'] = array( + $this->_channelInfo['servers']['primary'][$type]['function']); + } + $this->_channelInfo['servers']['primary'][$type]['function'][] = $set; + return true; + } + /** + * Add a protocol to a mirror's provides section + * @param string mirror name (server) + * @param string protocol type + * @param string protocol version + * @param string protocol name, if any + */ + function addMirrorFunction($mirror, $type, $version, $name = '') + { + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + $setmirror = false; + if (isset($this->_channelInfo['servers']['mirror'][0])) { + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + $setmirror = &$this->_channelInfo['servers']['mirror'][$i]; + break; + } + } + } else { + if ($this->_channelInfo['servers']['mirror']['attribs']['host'] == $mirror) { + $setmirror = &$this->_channelInfo['servers']['mirror']; + } + } + if (!$setmirror) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + $set = array('attribs' => array('version' => $version), '_content' => $name); + if (!isset($setmirror[$type]['function'])) { + $setmirror[$type]['function'] = $set; + $this->_isValid = false; + return true; + } elseif (!isset($setmirror[$type]['function'][0])) { + $setmirror[$type]['function'] = array($setmirror[$type]['function']); + } + $setmirror[$type]['function'][] = $set; + $this->_isValid = false; + return true; + } + + /** + * @param string Resource Type this url links to + * @param string URL + * @param string|false mirror name, if this is not a primary server REST base URL + */ + function setBaseURL($resourceType, $url, $mirror = false) + { + if ($mirror) { + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_validateError(PEAR_CHANNELFILE_ERROR_MIRROR_NOT_FOUND, + array('mirror' => $mirror)); + return false; + } + $setmirror = false; + if (isset($this->_channelInfo['servers']['mirror'][0])) { + foreach ($this->_channelInfo['servers']['mirror'] as $i => $mir) { + if ($mirror == $mir['attribs']['host']) { + $setmirror = &$this->_channelInfo['servers']['mirror'][$i]; + break; + } + } + } else { + if ($this->_channelInfo['servers']['mirror']['attribs']['host'] == $mirror) { + $setmirror = &$this->_channelInfo['servers']['mirror']; + } + } + } else { + $setmirror = &$this->_channelInfo['servers']['primary']; + } + $set = array('attribs' => array('type' => $resourceType), '_content' => $url); + if (!isset($setmirror['rest'])) { + $setmirror['rest'] = array(); + } + if (!isset($setmirror['rest']['baseurl'])) { + $setmirror['rest']['baseurl'] = $set; + $this->_isValid = false; + return true; + } elseif (!isset($setmirror['rest']['baseurl'][0])) { + $setmirror['rest']['baseurl'] = array($setmirror['rest']['baseurl']); + } + foreach ($setmirror['rest']['baseurl'] as $i => $url) { + if ($url['attribs']['type'] == $resourceType) { + $this->_isValid = false; + $setmirror['rest']['baseurl'][$i] = $set; + return true; + } + } + $setmirror['rest']['baseurl'][] = $set; + $this->_isValid = false; + return true; + } + + /** + * @param string mirror server + * @param int mirror http port + * @return boolean + */ + function addMirror($server, $port = null) + { + if ($this->_channelInfo['name'] == '__uri') { + return false; // the __uri channel cannot have mirrors by definition + } + $set = array('attribs' => array('host' => $server)); + if (is_numeric($port)) { + $set['attribs']['port'] = $port; + } + if (!isset($this->_channelInfo['servers']['mirror'])) { + $this->_channelInfo['servers']['mirror'] = $set; + return true; + } else { + if (!isset($this->_channelInfo['servers']['mirror'][0])) { + $this->_channelInfo['servers']['mirror'] = + array($this->_channelInfo['servers']['mirror']); + } + } + $this->_channelInfo['servers']['mirror'][] = $set; + return true; + } + + /** + * Retrieve the name of the validation package for this channel + * @return string|false + */ + function getValidationPackage() + { + if (!$this->_isValid && !$this->validate()) { + return false; + } + if (!isset($this->_channelInfo['validatepackage'])) { + return array('attribs' => array('version' => 'default'), + '_content' => 'PEAR_Validate'); + } + return $this->_channelInfo['validatepackage']; + } + + /** + * Retrieve the object that can be used for custom validation + * @param string|false the name of the package to validate. If the package is + * the channel validation package, PEAR_Validate is returned + * @return PEAR_Validate|false false is returned if the validation package + * cannot be located + */ + function &getValidationObject($package = false) + { + if (!class_exists('PEAR_Validate')) { + require_once 'PEAR/Validate.php'; + } + if (!$this->_isValid) { + if (!$this->validate()) { + $a = false; + return $a; + } + } + if (isset($this->_channelInfo['validatepackage'])) { + if ($package == $this->_channelInfo['validatepackage']) { + // channel validation packages are always validated by PEAR_Validate + $val = &new PEAR_Validate; + return $val; + } + if (!class_exists(str_replace('.', '_', + $this->_channelInfo['validatepackage']['_content']))) { + if ($this->isIncludeable(str_replace('_', '/', + $this->_channelInfo['validatepackage']['_content']) . '.php')) { + include_once str_replace('_', '/', + $this->_channelInfo['validatepackage']['_content']) . '.php'; + $vclass = str_replace('.', '_', + $this->_channelInfo['validatepackage']['_content']); + $val = &new $vclass; + } else { + $a = false; + return $a; + } + } else { + $vclass = str_replace('.', '_', + $this->_channelInfo['validatepackage']['_content']); + $val = &new $vclass; + } + } else { + $val = &new PEAR_Validate; + } + return $val; + } + + function isIncludeable($path) + { + $possibilities = explode(PATH_SEPARATOR, ini_get('include_path')); + foreach ($possibilities as $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $path) + && is_readable($dir . DIRECTORY_SEPARATOR . $path)) { + return true; + } + } + return false; + } + + /** + * This function is used by the channel updater and retrieves a value set by + * the registry, or the current time if it has not been set + * @return string + */ + function lastModified() + { + if (isset($this->_channelInfo['_lastmodified'])) { + return $this->_channelInfo['_lastmodified']; + } + return time(); + } +} +?> diff --git a/PEAR/ChannelFile/Parser.php b/PEAR/ChannelFile/Parser.php new file mode 100644 index 0000000..354a1b3 --- /dev/null +++ b/PEAR/ChannelFile/Parser.php @@ -0,0 +1,73 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Parser.php,v 1.4 2006/01/06 04:47:36 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * base xml parser class + */ +require_once 'PEAR/XMLParser.php'; +require_once 'PEAR/ChannelFile.php'; +/** + * Parser for channel.xml + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_ChannelFile_Parser extends PEAR_XMLParser +{ + var $_config; + var $_logger; + var $_registry; + + function setConfig(&$c) + { + $this->_config = &$c; + $this->_registry = &$c->getRegistry(); + } + + function setLogger(&$l) + { + $this->_logger = &$l; + } + + function parse($data, $file) + { + if (PEAR::isError($err = parent::parse($data, $file))) { + return $err; + } + $ret = new PEAR_ChannelFile; + $ret->setConfig($this->_config); + if (isset($this->_logger)) { + $ret->setLogger($this->_logger); + } + $ret->fromArray($this->_unserializedData); + // make sure the filelist is in the easy to read format needed + $ret->flattenFilelist(); + $ret->setPackagefile($file, $archive); + return $ret; + } +} +?> \ No newline at end of file diff --git a/PEAR/Command.php b/PEAR/Command.php new file mode 100644 index 0000000..3115e31 --- /dev/null +++ b/PEAR/Command.php @@ -0,0 +1,416 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Command.php,v 1.38 2006/10/31 02:54:40 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * Needed for error handling + */ +require_once 'PEAR.php'; +require_once 'PEAR/Frontend.php'; +require_once 'PEAR/XMLParser.php'; + +/** + * List of commands and what classes they are implemented in. + * @var array command => implementing class + */ +$GLOBALS['_PEAR_Command_commandlist'] = array(); + +/** + * List of commands and their descriptions + * @var array command => description + */ +$GLOBALS['_PEAR_Command_commanddesc'] = array(); + +/** + * List of shortcuts to common commands. + * @var array shortcut => command + */ +$GLOBALS['_PEAR_Command_shortcuts'] = array(); + +/** + * Array of command objects + * @var array class => object + */ +$GLOBALS['_PEAR_Command_objects'] = array(); + +/** + * PEAR command class, a simple factory class for administrative + * commands. + * + * How to implement command classes: + * + * - The class must be called PEAR_Command_Nnn, installed in the + * "PEAR/Common" subdir, with a method called getCommands() that + * returns an array of the commands implemented by the class (see + * PEAR/Command/Install.php for an example). + * + * - The class must implement a run() function that is called with three + * params: + * + * (string) command name + * (array) assoc array with options, freely defined by each + * command, for example: + * array('force' => true) + * (array) list of the other parameters + * + * The run() function returns a PEAR_CommandResponse object. Use + * these methods to get information: + * + * int getStatus() Returns PEAR_COMMAND_(SUCCESS|FAILURE|PARTIAL) + * *_PARTIAL means that you need to issue at least + * one more command to complete the operation + * (used for example for validation steps). + * + * string getMessage() Returns a message for the user. Remember, + * no HTML or other interface-specific markup. + * + * If something unexpected happens, run() returns a PEAR error. + * + * - DON'T OUTPUT ANYTHING! Return text for output instead. + * + * - DON'T USE HTML! The text you return will be used from both Gtk, + * web and command-line interfaces, so for now, keep everything to + * plain text. + * + * - DON'T USE EXIT OR DIE! Always use pear errors. From static + * classes do PEAR::raiseError(), from other classes do + * $this->raiseError(). + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command +{ + // {{{ factory() + + /** + * Get the right object for executing a command. + * + * @param string $command The name of the command + * @param object $config Instance of PEAR_Config object + * + * @return object the command object or a PEAR error + * + * @access public + * @static + */ + function &factory($command, &$config) + { + if (empty($GLOBALS['_PEAR_Command_commandlist'])) { + PEAR_Command::registerCommands(); + } + if (isset($GLOBALS['_PEAR_Command_shortcuts'][$command])) { + $command = $GLOBALS['_PEAR_Command_shortcuts'][$command]; + } + if (!isset($GLOBALS['_PEAR_Command_commandlist'][$command])) { + $a = PEAR::raiseError("unknown command `$command'"); + return $a; + } + $class = $GLOBALS['_PEAR_Command_commandlist'][$command]; + if (!class_exists($class)) { + require_once $GLOBALS['_PEAR_Command_objects'][$class]; + } + if (!class_exists($class)) { + $a = PEAR::raiseError("unknown command `$command'"); + return $a; + } + $ui =& PEAR_Command::getFrontendObject(); + $obj = &new $class($ui, $config); + return $obj; + } + + // }}} + // {{{ & getObject() + function &getObject($command) + { + $class = $GLOBALS['_PEAR_Command_commandlist'][$command]; + if (!class_exists($class)) { + require_once $GLOBALS['_PEAR_Command_objects'][$class]; + } + if (!class_exists($class)) { + return PEAR::raiseError("unknown command `$command'"); + } + $ui =& PEAR_Command::getFrontendObject(); + $config = &PEAR_Config::singleton(); + $obj = &new $class($ui, $config); + return $obj; + } + + // }}} + // {{{ & getFrontendObject() + + /** + * Get instance of frontend object. + * + * @return object|PEAR_Error + * @static + */ + function &getFrontendObject() + { + $a = &PEAR_Frontend::singleton(); + return $a; + } + + // }}} + // {{{ & setFrontendClass() + + /** + * Load current frontend class. + * + * @param string $uiclass Name of class implementing the frontend + * + * @return object the frontend object, or a PEAR error + * @static + */ + function &setFrontendClass($uiclass) + { + $a = &PEAR_Frontend::setFrontendClass($uiclass); + return $a; + } + + // }}} + // {{{ setFrontendType() + + /** + * Set current frontend. + * + * @param string $uitype Name of the frontend type (for example "CLI") + * + * @return object the frontend object, or a PEAR error + * @static + */ + function setFrontendType($uitype) + { + $uiclass = 'PEAR_Frontend_' . $uitype; + return PEAR_Command::setFrontendClass($uiclass); + } + + // }}} + // {{{ registerCommands() + + /** + * Scan through the Command directory looking for classes + * and see what commands they implement. + * + * @param bool (optional) if FALSE (default), the new list of + * commands should replace the current one. If TRUE, + * new entries will be merged with old. + * + * @param string (optional) where (what directory) to look for + * classes, defaults to the Command subdirectory of + * the directory from where this file (__FILE__) is + * included. + * + * @return bool TRUE on success, a PEAR error on failure + * + * @access public + * @static + */ + function registerCommands($merge = false, $dir = null) + { + $parser = new PEAR_XMLParser; + if ($dir === null) { + $dir = dirname(__FILE__) . '/Command'; + } + if (!is_dir($dir)) { + return PEAR::raiseError("registerCommands: opendir($dir) '$dir' does not exist or is not a directory"); + } + $dp = @opendir($dir); + if (empty($dp)) { + return PEAR::raiseError("registerCommands: opendir($dir) failed"); + } + if (!$merge) { + $GLOBALS['_PEAR_Command_commandlist'] = array(); + } + while ($entry = readdir($dp)) { + if ($entry{0} == '.' || substr($entry, -4) != '.xml') { + continue; + } + $class = "PEAR_Command_".substr($entry, 0, -4); + $file = "$dir/$entry"; + $parser->parse(file_get_contents($file)); + $implements = $parser->getData(); + // List of commands + if (empty($GLOBALS['_PEAR_Command_objects'][$class])) { + $GLOBALS['_PEAR_Command_objects'][$class] = "$dir/" . substr($entry, 0, -4) . + '.php'; + } + foreach ($implements as $command => $desc) { + if ($command == 'attribs') { + continue; + } + if (isset($GLOBALS['_PEAR_Command_commandlist'][$command])) { + return PEAR::raiseError('Command "' . $command . '" already registered in ' . + 'class "' . $GLOBALS['_PEAR_Command_commandlist'][$command] . '"'); + } + $GLOBALS['_PEAR_Command_commandlist'][$command] = $class; + $GLOBALS['_PEAR_Command_commanddesc'][$command] = $desc['summary']; + if (isset($desc['shortcut'])) { + $shortcut = $desc['shortcut']; + if (isset($GLOBALS['_PEAR_Command_shortcuts'][$shortcut])) { + return PEAR::raiseError('Command shortcut "' . $shortcut . '" already ' . + 'registered to command "' . $command . '" in class "' . + $GLOBALS['_PEAR_Command_commandlist'][$command] . '"'); + } + $GLOBALS['_PEAR_Command_shortcuts'][$shortcut] = $command; + } + if (isset($desc['options']) && $desc['options']) { + foreach ($desc['options'] as $oname => $option) { + if (isset($option['shortopt']) && strlen($option['shortopt']) > 1) { + return PEAR::raiseError('Option "' . $oname . '" short option "' . + $option['shortopt'] . '" must be ' . + 'only 1 character in Command "' . $command . '" in class "' . + $class . '"'); + } + } + } + } + } + ksort($GLOBALS['_PEAR_Command_shortcuts']); + ksort($GLOBALS['_PEAR_Command_commandlist']); + @closedir($dp); + return true; + } + + // }}} + // {{{ getCommands() + + /** + * Get the list of currently supported commands, and what + * classes implement them. + * + * @return array command => implementing class + * + * @access public + * @static + */ + function getCommands() + { + if (empty($GLOBALS['_PEAR_Command_commandlist'])) { + PEAR_Command::registerCommands(); + } + return $GLOBALS['_PEAR_Command_commandlist']; + } + + // }}} + // {{{ getShortcuts() + + /** + * Get the list of command shortcuts. + * + * @return array shortcut => command + * + * @access public + * @static + */ + function getShortcuts() + { + if (empty($GLOBALS['_PEAR_Command_shortcuts'])) { + PEAR_Command::registerCommands(); + } + return $GLOBALS['_PEAR_Command_shortcuts']; + } + + // }}} + // {{{ getGetoptArgs() + + /** + * Compiles arguments for getopt. + * + * @param string $command command to get optstring for + * @param string $short_args (reference) short getopt format + * @param array $long_args (reference) long getopt format + * + * @return void + * + * @access public + * @static + */ + function getGetoptArgs($command, &$short_args, &$long_args) + { + if (empty($GLOBALS['_PEAR_Command_commandlist'])) { + PEAR_Command::registerCommands(); + } + if (isset($GLOBALS['_PEAR_Command_shortcuts'][$command])) { + $command = $GLOBALS['_PEAR_Command_shortcuts'][$command]; + } + if (!isset($GLOBALS['_PEAR_Command_commandlist'][$command])) { + return null; + } + $obj = &PEAR_Command::getObject($command); + return $obj->getGetoptArgs($command, $short_args, $long_args); + } + + // }}} + // {{{ getDescription() + + /** + * Get description for a command. + * + * @param string $command Name of the command + * + * @return string command description + * + * @access public + * @static + */ + function getDescription($command) + { + if (!isset($GLOBALS['_PEAR_Command_commanddesc'][$command])) { + return null; + } + return $GLOBALS['_PEAR_Command_commanddesc'][$command]; + } + + // }}} + // {{{ getHelp() + + /** + * Get help for command. + * + * @param string $command Name of the command to return help for + * + * @access public + * @static + */ + function getHelp($command) + { + $cmds = PEAR_Command::getCommands(); + if (isset($GLOBALS['_PEAR_Command_shortcuts'][$command])) { + $command = $GLOBALS['_PEAR_Command_shortcuts'][$command]; + } + if (isset($cmds[$command])) { + $obj = &PEAR_Command::getObject($command); + return $obj->getHelp($command); + } + return false; + } + // }}} +} + +?> diff --git a/PEAR/Command/Auth.php b/PEAR/Command/Auth.php new file mode 100644 index 0000000..fd971c7 --- /dev/null +++ b/PEAR/Command/Auth.php @@ -0,0 +1,203 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Auth.php,v 1.30 2007/05/20 00:16:44 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; +require_once 'PEAR/Config.php'; + +/** + * PEAR commands for login/logout + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Auth extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'login' => array( + 'summary' => 'Connects and authenticates to remote server', + 'shortcut' => 'li', + 'function' => 'doLogin', + 'options' => array(), + 'doc' => ' +Log in to a remote channel server. If is not supplied, +the default channel is used. To use remote functions in the installer +that require any kind of privileges, you need to log in first. The +username and password you enter here will be stored in your per-user +PEAR configuration (~/.pearrc on Unix-like systems). After logging +in, your username and password will be sent along in subsequent +operations on the remote server.', + ), + 'logout' => array( + 'summary' => 'Logs out from the remote server', + 'shortcut' => 'lo', + 'function' => 'doLogout', + 'options' => array(), + 'doc' => ' +Logs out from the remote server. This command does not actually +connect to the remote server, it only deletes the stored username and +password from your user configuration.', + ) + + ); + + // }}} + + // {{{ constructor + + /** + * PEAR_Command_Auth constructor. + * + * @access public + */ + function PEAR_Command_Auth(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ doLogin() + + /** + * Execute the 'login' command. + * + * @param string $command command name + * + * @param array $options option_name => value + * + * @param array $params list of additional parameters + * + * @return bool TRUE on success or + * a PEAR error on failure + * + * @access public + */ + function doLogin($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + + // If a parameter is supplied, use that as the channel to log in to + if (isset($params[0])) { + $channel = $params[0]; + } else { + $channel = $this->config->get('default_channel'); + } + + $chan = $reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $this->raiseError($chan); + } + $server = $this->config->get('preferred_mirror', null, $channel); + $remote = &$this->config->getRemote(); + $username = $this->config->get('username', null, $channel); + if (empty($username)) { + $username = isset($_ENV['USER']) ? $_ENV['USER'] : null; + } + $this->ui->outputData("Logging in to $server.", $command); + + list($username, $password) = $this->ui->userDialog( + $command, + array('Username', 'Password'), + array('text', 'password'), + array($username, '') + ); + $username = trim($username); + $password = trim($password); + + $ourfile = $this->config->getConfFile('user'); + if (!$ourfile) { + $ourfile = $this->config->getConfFile('system'); + } + + $this->config->set('username', $username, 'user', $channel); + $this->config->set('password', $password, 'user', $channel); + + if ($chan->supportsREST()) { + $ok = true; + } else { + $remote->expectError(401); + $ok = $remote->call('logintest'); + $remote->popExpect(); + } + if ($ok === true) { + $this->ui->outputData("Logged in.", $command); + // avoid changing any temporary settings changed with -d + $ourconfig = new PEAR_Config($ourfile, $ourfile); + $ourconfig->set('username', $username, 'user', $channel); + $ourconfig->set('password', $password, 'user', $channel); + $ourconfig->store(); + } else { + return $this->raiseError("Login failed!"); + } + return true; + } + + // }}} + // {{{ doLogout() + + /** + * Execute the 'logout' command. + * + * @param string $command command name + * + * @param array $options option_name => value + * + * @param array $params list of additional parameters + * + * @return bool TRUE on success or + * a PEAR error on failure + * + * @access public + */ + function doLogout($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $channel = $this->config->get('default_channel'); + $chan = $reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $this->raiseError($chan); + } + $server = $this->config->get('preferred_mirror'); + $this->ui->outputData("Logging out from $server.", $command); + $this->config->remove('username'); + $this->config->remove('password'); + $this->config->store(); + return true; + } + + // }}} +} + +?> \ No newline at end of file diff --git a/PEAR/Command/Auth.xml b/PEAR/Command/Auth.xml new file mode 100644 index 0000000..17e3b34 --- /dev/null +++ b/PEAR/Command/Auth.xml @@ -0,0 +1,26 @@ + + + Connects and authenticates to remote server + li + doLogin + + <channel name> +Log in to a remote channel server. <channel name> is not supplied, +the default channel is used. To use remote functions in the installer +that require any kind of privileges, you need to log in first. The +username and password you enter here will be stored in your per-user +PEAR configuration (~/.pearrc on Unix-like systems). After logging +in, your username and password will be sent along in subsequent +operations on the remote server. + + + Logs out from the remote server + lo + doLogout + + +Logs out from the remote server. This command does not actually +connect to the remote server, it only deletes the stored username and +password from your user configuration. + + \ No newline at end of file diff --git a/PEAR/Command/Build.php b/PEAR/Command/Build.php new file mode 100644 index 0000000..a603fcb --- /dev/null +++ b/PEAR/Command/Build.php @@ -0,0 +1,104 @@ + + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Build.php,v 1.13 2006/01/06 04:47:36 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for building extensions. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Build extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'build' => array( + 'summary' => 'Build an Extension From C Source', + 'function' => 'doBuild', + 'shortcut' => 'b', + 'options' => array(), + 'doc' => '[package.xml] +Builds one or more extensions contained in a package.' + ), + ); + + // }}} + + // {{{ constructor + + /** + * PEAR_Command_Build constructor. + * + * @access public + */ + function PEAR_Command_Build(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ doBuild() + + function doBuild($command, $options, $params) + { + require_once 'PEAR/Builder.php'; + if (sizeof($params) < 1) { + $params[0] = 'package.xml'; + } + $builder = &new PEAR_Builder($this->ui); + $this->debug = $this->config->get('verbose'); + $err = $builder->build($params[0], array(&$this, 'buildCallback')); + if (PEAR::isError($err)) { + return $err; + } + return true; + } + + // }}} + // {{{ buildCallback() + + function buildCallback($what, $data) + { + if (($what == 'cmdoutput' && $this->debug > 1) || + ($what == 'output' && $this->debug > 0)) { + $this->ui->outputData(rtrim($data), 'build'); + } + } + + // }}} +} diff --git a/PEAR/Command/Build.xml b/PEAR/Command/Build.xml new file mode 100644 index 0000000..ec4e6f5 --- /dev/null +++ b/PEAR/Command/Build.xml @@ -0,0 +1,10 @@ + + + Build an Extension From C Source + doBuild + b + + [package.xml] +Builds one or more extensions contained in a package. + + \ No newline at end of file diff --git a/PEAR/Command/Channels.php b/PEAR/Command/Channels.php new file mode 100644 index 0000000..21997c5 --- /dev/null +++ b/PEAR/Command/Channels.php @@ -0,0 +1,737 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Channels.php,v 1.56 2007/06/10 04:34:19 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for managing channels. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Command_Channels extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'list-channels' => array( + 'summary' => 'List Available Channels', + 'function' => 'doList', + 'shortcut' => 'lc', + 'options' => array(), + 'doc' => ' +List all available channels for installation. +', + ), + 'update-channels' => array( + 'summary' => 'Update the Channel List', + 'function' => 'doUpdateAll', + 'shortcut' => 'uc', + 'options' => array(), + 'doc' => ' +List all installed packages in all channels. +' + ), + 'channel-delete' => array( + 'summary' => 'Remove a Channel From the List', + 'function' => 'doDelete', + 'shortcut' => 'cde', + 'options' => array(), + 'doc' => ' +Delete a channel from the registry. You may not +remove any channel that has installed packages. +' + ), + 'channel-add' => array( + 'summary' => 'Add a Channel', + 'function' => 'doAdd', + 'shortcut' => 'ca', + 'options' => array(), + 'doc' => ' +Add a private channel to the channel list. Note that all +public channels should be synced using "update-channels". +Parameter may be either a local file or remote URL to a +channel.xml. +' + ), + 'channel-update' => array( + 'summary' => 'Update an Existing Channel', + 'function' => 'doUpdate', + 'shortcut' => 'cu', + 'options' => array( + 'force' => array( + 'shortopt' => 'f', + 'doc' => 'will force download of new channel.xml if an existing channel name is used', + ), + 'channel' => array( + 'shortopt' => 'c', + 'arg' => 'CHANNEL', + 'doc' => 'will force download of new channel.xml if an existing channel name is used', + ), +), + 'doc' => '[|] +Update a channel in the channel list directly. Note that all +public channels can be synced using "update-channels". +Parameter may be a local or remote channel.xml, or the name of +an existing channel. +' + ), + 'channel-info' => array( + 'summary' => 'Retrieve Information on a Channel', + 'function' => 'doInfo', + 'shortcut' => 'ci', + 'options' => array(), + 'doc' => ' +List the files in an installed package. +' + ), + 'channel-alias' => array( + 'summary' => 'Specify an alias to a channel name', + 'function' => 'doAlias', + 'shortcut' => 'cha', + 'options' => array(), + 'doc' => ' +Specify a specific alias to use for a channel name. +The alias may not be an existing channel name or +alias. +' + ), + 'channel-discover' => array( + 'summary' => 'Initialize a Channel from its server', + 'function' => 'doDiscover', + 'shortcut' => 'di', + 'options' => array(), + 'doc' => '[|] +Initialize a channel from its server and create a local channel.xml. +If is in the format ":@" then + and will be set as the login username/password for +. Use caution when passing the username/password in this way, as +it may allow other users on your computer to briefly view your username/ +password via the system\'s process list. +' + ), + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Registry constructor. + * + * @access public + */ + function PEAR_Command_Channels(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ doList() + + function _sortChannels($a, $b) + { + return strnatcasecmp($a->getName(), $b->getName()); + } + + function doList($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $registered = $reg->getChannels(); + usort($registered, array(&$this, '_sortchannels')); + $i = $j = 0; + $data = array( + 'caption' => 'Registered Channels:', + 'border' => true, + 'headline' => array('Channel', 'Summary') + ); + foreach ($registered as $channel) { + $data['data'][] = array($channel->getName(), + $channel->getSummary()); + } + if (count($registered)==0) { + $data = '(no registered channels)'; + } + $this->ui->outputData($data, $command); + return true; + } + + function doUpdateAll($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $channels = $reg->getChannels(); + + $success = true; + foreach ($channels as $channel) { + if ($channel->getName() != '__uri') { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->doUpdate('channel-update', + $options, + array($channel->getName())); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage(), $command); + $success = false; + } else { + $success &= $err; + } + } + } + return $success; + } + + function doInfo($command, $options, $params) + { + if (sizeof($params) != 1) { + return $this->raiseError("No channel specified"); + } + $reg = &$this->config->getRegistry(); + $channel = strtolower($params[0]); + if ($reg->channelExists($channel)) { + $chan = $reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $this->raiseError($chan); + } + } else { + if (strpos($channel, '://')) { + $downloader = &$this->getDownloader(); + $tmpdir = $this->config->get('temp_dir'); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $loc = $downloader->downloadHttp($channel, $this->ui, $tmpdir); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($loc)) { + return $this->raiseError('Cannot open "' . $channel . + '" (' . $loc->getMessage() . ')'); + } else { + $contents = implode('', file($loc)); + } + } else { + if (file_exists($params[0])) { + $fp = fopen($params[0], 'r'); + if (!$fp) { + return $this->raiseError('Cannot open "' . $params[0] . '"'); + } + } else { + return $this->raiseError('Unknown channel "' . $channel . '"'); + } + $contents = ''; + while (!feof($fp)) { + $contents .= fread($fp, 1024); + } + fclose($fp); + } + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $chan = new PEAR_ChannelFile; + $chan->fromXmlString($contents); + $chan->validate(); + if ($errs = $chan->getErrors(true)) { + foreach ($errs as $err) { + $this->ui->outputData($err['level'] . ': ' . $err['message']); + } + return $this->raiseError('Channel file "' . $params[0] . '" is not valid'); + } + } + if ($chan) { + $channel = $chan->getName(); + $caption = 'Channel ' . $channel . ' Information:'; + $data1 = array( + 'caption' => $caption, + 'border' => true); + $data1['data']['server'] = array('Name and Server', $chan->getName()); + if ($chan->getAlias() != $chan->getName()) { + $data1['data']['alias'] = array('Alias', $chan->getAlias()); + } + $data1['data']['summary'] = array('Summary', $chan->getSummary()); + $validate = $chan->getValidationPackage(); + $data1['data']['vpackage'] = array('Validation Package Name', $validate['_content']); + $data1['data']['vpackageversion'] = + array('Validation Package Version', $validate['attribs']['version']); + $d = array(); + $d['main'] = $data1; + + $data['data'] = array(); + $data['caption'] = 'Server Capabilities'; + $data['headline'] = array('Type', 'Version/REST type', 'Function Name/REST base'); + $capabilities = $chan->getFunctions('xmlrpc'); + $soaps = $chan->getFunctions('soap'); + if ($capabilities || $soaps || $chan->supportsREST()) { + if ($capabilities) { + if (!isset($capabilities[0])) { + $capabilities = array($capabilities); + } + foreach ($capabilities as $protocol) { + $data['data'][] = array('xmlrpc', $protocol['attribs']['version'], + $protocol['_content']); + } + } + if ($soaps) { + if (!isset($soaps[0])) { + $soaps = array($soaps); + } + foreach ($soaps as $protocol) { + $data['data'][] = array('soap', $protocol['attribs']['version'], + $protocol['_content']); + } + } + if ($chan->supportsREST()) { + $funcs = $chan->getFunctions('rest'); + if (!isset($funcs[0])) { + $funcs = array($funcs); + } + foreach ($funcs as $protocol) { + $data['data'][] = array('rest', $protocol['attribs']['type'], + $protocol['_content']); + } + } + } else { + $data['data'][] = array('No supported protocols'); + } + $d['protocols'] = $data; + $data['data'] = array(); + $mirrors = $chan->getMirrors(); + if ($mirrors) { + $data['caption'] = 'Channel ' . $channel . ' Mirrors:'; + unset($data['headline']); + foreach ($mirrors as $mirror) { + $data['data'][] = array($mirror['attribs']['host']); + $d['mirrors'] = $data; + } + foreach ($mirrors as $i => $mirror) { + $data['data'] = array(); + $data['caption'] = 'Mirror ' . $mirror['attribs']['host'] . ' Capabilities'; + $data['headline'] = array('Type', 'Version/REST type', 'Function Name/REST base'); + $capabilities = $chan->getFunctions('xmlrpc', $mirror['attribs']['host']); + $soaps = $chan->getFunctions('soap', $mirror['attribs']['host']); + if ($capabilities || $soaps || $chan->supportsREST($mirror['attribs']['host'])) { + if ($capabilities) { + if (!isset($capabilities[0])) { + $capabilities = array($capabilities); + } + foreach ($capabilities as $protocol) { + $data['data'][] = array('xmlrpc', $protocol['attribs']['version'], + $protocol['_content']); + } + } + if ($soaps) { + if (!isset($soaps[0])) { + $soaps = array($soaps); + } + foreach ($soaps as $protocol) { + $data['data'][] = array('soap', $protocol['attribs']['version'], + $protocol['_content']); + } + } + if ($chan->supportsREST($mirror['attribs']['host'])) { + $funcs = $chan->getFunctions('rest', $mirror['attribs']['host']); + if (!isset($funcs[0])) { + $funcs = array($funcs); + } + foreach ($funcs as $protocol) { + $data['data'][] = array('rest', $protocol['attribs']['type'], + $protocol['_content']); + } + } + } else { + $data['data'][] = array('No supported protocols'); + } + $d['mirrorprotocols' . $i] = $data; + } + } + $this->ui->outputData($d, 'channel-info'); + } else { + return $this->raiseError('Serious error: Channel "' . $params[0] . + '" has a corrupted registry entry'); + } + } + + // }}} + + function doDelete($command, $options, $params) + { + if (sizeof($params) != 1) { + return $this->raiseError('channel-delete: no channel specified'); + } + $reg = &$this->config->getRegistry(); + if (!$reg->channelExists($params[0])) { + return $this->raiseError('channel-delete: channel "' . $params[0] . '" does not exist'); + } + $channel = $reg->channelName($params[0]); + if ($channel == 'pear.php.net') { + return $this->raiseError('Cannot delete the pear.php.net channel'); + } + if ($channel == 'pecl.php.net') { + return $this->raiseError('Cannot delete the pecl.php.net channel'); + } + if ($channel == '__uri') { + return $this->raiseError('Cannot delete the __uri pseudo-channel'); + } + if (PEAR::isError($err = $reg->listPackages($channel))) { + return $err; + } + if (count($err)) { + return $this->raiseError('Channel "' . $channel . + '" has installed packages, cannot delete'); + } + if (!$reg->deleteChannel($channel)) { + return $this->raiseError('Channel "' . $channel . '" deletion failed'); + } else { + $this->config->deleteChannel($channel); + $this->ui->outputData('Channel "' . $channel . '" deleted', $command); + } + } + + function doAdd($command, $options, $params) + { + if (sizeof($params) != 1) { + return $this->raiseError('channel-add: no channel file specified'); + } + if (strpos($params[0], '://')) { + $downloader = &$this->getDownloader(); + $tmpdir = $this->config->get('temp_dir'); + if (!file_exists($tmpdir)) { + require_once 'System.php'; + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = System::mkdir(array('-p', $tmpdir)); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($err)) { + return $this->raiseError('channel-add: temp_dir does not exist: "' . + $tmpdir . + '" - You can change this location with "pear config-set temp_dir"'); + } + } + if (!is_writable($tmpdir)) { + return $this->raiseError('channel-add: temp_dir is not writable: "' . + $tmpdir . + '" - You can change this location with "pear config-set temp_dir"'); + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $loc = $downloader->downloadHttp($params[0], $this->ui, $tmpdir, null, false); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($loc)) { + return $this->raiseError('channel-add: Cannot open "' . $params[0] . + '" (' . $loc->getMessage() . ')'); + } else { + list($loc, $lastmodified) = $loc; + $contents = implode('', file($loc)); + } + } else { + $lastmodified = $fp = false; + if (file_exists($params[0])) { + $fp = fopen($params[0], 'r'); + } + if (!$fp) { + return $this->raiseError('channel-add: cannot open "' . $params[0] . '"'); + } + $contents = ''; + while (!feof($fp)) { + $contents .= fread($fp, 1024); + } + fclose($fp); + } + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $channel = new PEAR_ChannelFile; + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $result = $channel->fromXmlString($contents); + PEAR::staticPopErrorHandling(); + if (!$result) { + $exit = false; + if (count($errors = $channel->getErrors(true))) { + foreach ($errors as $error) { + $this->ui->outputData(ucfirst($error['level'] . ': ' . $error['message'])); + if (!$exit) { + $exit = $error['level'] == 'error' ? true : false; + } + } + if ($exit) { + return $this->raiseError('channel-add: invalid channel.xml file'); + } + } + } + $reg = &$this->config->getRegistry(); + if ($reg->channelExists($channel->getName())) { + return $this->raiseError('channel-add: Channel "' . $channel->getName() . + '" exists, use channel-update to update entry'); + } + $ret = $reg->addChannel($channel, $lastmodified); + if (PEAR::isError($ret)) { + return $ret; + } + if (!$ret) { + return $this->raiseError('channel-add: adding Channel "' . $channel->getName() . + '" to registry failed'); + } + $this->config->setChannels($reg->listChannels()); + $this->config->writeConfigFile(); + $this->ui->outputData('Adding Channel "' . $channel->getName() . '" succeeded', $command); + } + + function doUpdate($command, $options, $params) + { + $tmpdir = $this->config->get('temp_dir'); + if (!file_exists($tmpdir)) { + require_once 'System.php'; + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = System::mkdir(array('-p', $tmpdir)); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($err)) { + return $this->raiseError('channel-add: temp_dir does not exist: "' . + $tmpdir . + '" - You can change this location with "pear config-set temp_dir"'); + } + } + if (!is_writable($tmpdir)) { + return $this->raiseError('channel-add: temp_dir is not writable: "' . + $tmpdir . + '" - You can change this location with "pear config-set temp_dir"'); + } + $reg = &$this->config->getRegistry(); + if (sizeof($params) != 1) { + return $this->raiseError("No channel file specified"); + } + $lastmodified = false; + if ((!file_exists($params[0]) || is_dir($params[0])) + && $reg->channelExists(strtolower($params[0]))) { + $c = $reg->getChannel(strtolower($params[0])); + if (PEAR::isError($c)) { + return $this->raiseError($c); + } + $this->ui->outputData("Updating channel \"$params[0]\"", $command); + $dl = &$this->getDownloader(array()); + // if force is specified, use a timestamp of "1" to force retrieval + $lastmodified = isset($options['force']) ? false : $c->lastModified(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $contents = $dl->downloadHttp('http://' . $c->getName() . '/channel.xml', + $this->ui, $tmpdir, null, $lastmodified); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($contents)) { + return $this->raiseError('Cannot retrieve channel.xml for channel "' . + $c->getName() . '" (' . $contents->getMessage() . ')'); + } + list($contents, $lastmodified) = $contents; + if (!$contents) { + $this->ui->outputData("Channel \"$params[0]\" is up to date"); + return; + } + $contents = implode('', file($contents)); + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $channel = new PEAR_ChannelFile; + $channel->fromXmlString($contents); + if (!$channel->getErrors()) { + // security check: is the downloaded file for the channel we got it from? + if (strtolower($channel->getName()) != strtolower($c->getName())) { + if (isset($options['force'])) { + $this->ui->log(0, 'WARNING: downloaded channel definition file' . + ' for channel "' . $channel->getName() . '" from channel "' . + strtolower($c->getName()) . '"'); + } else { + return $this->raiseError('ERROR: downloaded channel definition file' . + ' for channel "' . $channel->getName() . '" from channel "' . + strtolower($c->getName()) . '"'); + } + } + } + } else { + if (strpos($params[0], '://')) { + $dl = &$this->getDownloader(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $loc = $dl->downloadHttp($params[0], + $this->ui, $tmpdir, null, $lastmodified); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($loc)) { + return $this->raiseError("Cannot open " . $params[0] . + ' (' . $loc->getMessage() . ')'); + } else { + list($loc, $lastmodified) = $loc; + $contents = implode('', file($loc)); + } + } else { + $fp = false; + if (file_exists($params[0])) { + $fp = fopen($params[0], 'r'); + } + if (!$fp) { + return $this->raiseError("Cannot open " . $params[0]); + } + $contents = ''; + while (!feof($fp)) { + $contents .= fread($fp, 1024); + } + fclose($fp); + } + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $channel = new PEAR_ChannelFile; + $channel->fromXmlString($contents); + } + $exit = false; + if (count($errors = $channel->getErrors(true))) { + foreach ($errors as $error) { + $this->ui->outputData(ucfirst($error['level'] . ': ' . $error['message'])); + if (!$exit) { + $exit = $error['level'] == 'error' ? true : false; + } + } + if ($exit) { + return $this->raiseError('Invalid channel.xml file'); + } + } + if (!$reg->channelExists($channel->getName())) { + return $this->raiseError('Error: Channel "' . $channel->getName() . + '" does not exist, use channel-add to add an entry'); + } + $ret = $reg->updateChannel($channel, $lastmodified); + if (PEAR::isError($ret)) { + return $ret; + } + if (!$ret) { + return $this->raiseError('Updating Channel "' . $channel->getName() . + '" in registry failed'); + } + $this->config->setChannels($reg->listChannels()); + $this->config->writeConfigFile(); + $this->ui->outputData('Update of Channel "' . $channel->getName() . '" succeeded'); + } + + function &getDownloader() + { + if (!class_exists('PEAR_Downloader')) { + require_once 'PEAR/Downloader.php'; + } + $a = new PEAR_Downloader($this->ui, array(), $this->config); + return $a; + } + + function doAlias($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + if (sizeof($params) == 1) { + return $this->raiseError('No channel alias specified'); + } + if (sizeof($params) != 2) { + return $this->raiseError( + 'Invalid format, correct is: channel-alias channel alias'); + } + if (!$reg->channelExists($params[0], true)) { + if ($reg->isAlias($params[0])) { + $extra = ' (use "channel-alias ' . $reg->channelName($params[0]) . ' ' . + strtolower($params[1]) . '")'; + } else { + $extra = ''; + } + return $this->raiseError('"' . $params[0] . '" is not a valid channel' . $extra); + } + if ($reg->isAlias($params[1])) { + return $this->raiseError('Channel "' . $reg->channelName($params[1]) . '" is ' . + 'already aliased to "' . strtolower($params[1]) . '", cannot re-alias'); + } + $chan = &$reg->getChannel($params[0]); + if (PEAR::isError($chan)) { + return $this->raiseError('Corrupt registry? Error retrieving channel "' . $params[0] . + '" information (' . $chan->getMessage() . ')'); + } + // make it a local alias + if (!$chan->setAlias(strtolower($params[1]), true)) { + return $this->raiseError('Alias "' . strtolower($params[1]) . + '" is not a valid channel alias'); + } + $reg->updateChannel($chan); + $this->ui->outputData('Channel "' . $chan->getName() . '" aliased successfully to "' . + strtolower($params[1]) . '"'); + } + + /** + * The channel-discover command + * + * @param string $command command name + * @param array $options option_name => value + * @param array $params list of additional parameters. + * $params[0] should contain a string with either: + * - or + * - :@ + * @return null|PEAR_Error + */ + function doDiscover($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + if (sizeof($params) != 1) { + return $this->raiseError("No channel server specified"); + } + + // Look for the possible input format ":@" + if (preg_match('/^(.+):(.+)@(.+)\\z/', $params[0], $matches)) { + $username = $matches[1]; + $password = $matches[2]; + $channel = $matches[3]; + } else { + $channel = $params[0]; + } + + if ($reg->channelExists($channel)) { + if ($reg->isAlias($channel)) { + return $this->raiseError("A channel alias named \"$channel\" " . + 'already exists, aliasing channel "' . $reg->channelName($channel) + . '"'); + } else { + return $this->raiseError("Channel \"$channel\" is already initialized"); + } + } + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->doAdd($command, $options, array('http://' . $channel . '/channel.xml')); + $this->popErrorHandling(); + if (PEAR::isError($err)) { + return $this->raiseError("Discovery of channel \"$channel\" failed (" . + $err->getMessage() . ')'); + } + + // Store username/password if they were given + // Arguably we should do a logintest on the channel here, but since + // that's awkward on a REST-based channel (even "pear login" doesn't + // do it for those), and XML-RPC is deprecated, it's fairly pointless. + if (isset($username)) { + $this->config->set('username', $username, 'user', $channel); + $this->config->set('password', $password, 'user', $channel); + $this->config->store(); + $this->ui->outputData("Stored login for channel \"$channel\" using username \"$username\"", $command); + } + + $this->ui->outputData("Discovery of channel \"$channel\" succeeded", $command); + } +} +?> diff --git a/PEAR/Command/Channels.xml b/PEAR/Command/Channels.xml new file mode 100644 index 0000000..e7c7b7f --- /dev/null +++ b/PEAR/Command/Channels.xml @@ -0,0 +1,98 @@ + + + List Available Channels + doList + lc + + +List all available channels for installation. + + + + Update the Channel List + doUpdateAll + uc + + +List all installed packages in all channels. + + + + Remove a Channel From the List + doDelete + cde + + <channel name> +Delete a channel from the registry. You may not +remove any channel that has installed packages. + + + + Add a Channel + doAdd + ca + + <channel.xml> +Add a private channel to the channel list. Note that all +public channels should be synced using "update-channels". +Parameter may be either a local file or remote URL to a +channel.xml. + + + + Update an Existing Channel + doUpdate + cu + + + f + will force download of new channel.xml if an existing channel name is used + + + c + CHANNEL + will force download of new channel.xml if an existing channel name is used + + + [<channel.xml>|<channel name>] +Update a channel in the channel list directly. Note that all +public channels can be synced using "update-channels". +Parameter may be a local or remote channel.xml, or the name of +an existing channel. + + + + Retrieve Information on a Channel + doInfo + ci + + <package> +List the files in an installed package. + + + + Specify an alias to a channel name + doAlias + cha + + <channel> <alias> +Specify a specific alias to use for a channel name. +The alias may not be an existing channel name or +alias. + + + + Initialize a Channel from its server + doDiscover + di + + [<channel.xml>|<channel name>] +Initialize a channel from its server and create a local channel.xml. +If <channel name> is in the format "<username>:<password>@<channel>" then +<username> and <password> will be set as the login username/password for +<channel>. Use caution when passing the username/password in this way, as +it may allow other users on your computer to briefly view your username/ +password via the system's process list. + + + diff --git a/PEAR/Command/Common.php b/PEAR/Command/Common.php new file mode 100644 index 0000000..b8641cf --- /dev/null +++ b/PEAR/Command/Common.php @@ -0,0 +1,291 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Common.php,v 1.35 2006/06/08 22:25:18 pajoye Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR.php'; + +/** + * PEAR commands base class + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Common extends PEAR +{ + // {{{ properties + + /** + * PEAR_Config object used to pass user system and configuration + * on when executing commands + * + * @var PEAR_Config + */ + var $config; + /** + * @var PEAR_Registry + * @access protected + */ + var $_registry; + + /** + * User Interface object, for all interaction with the user. + * @var object + */ + var $ui; + + var $_deps_rel_trans = array( + 'lt' => '<', + 'le' => '<=', + 'eq' => '=', + 'ne' => '!=', + 'gt' => '>', + 'ge' => '>=', + 'has' => '==' + ); + + var $_deps_type_trans = array( + 'pkg' => 'package', + 'ext' => 'extension', + 'php' => 'PHP', + 'prog' => 'external program', + 'ldlib' => 'external library for linking', + 'rtlib' => 'external runtime library', + 'os' => 'operating system', + 'websrv' => 'web server', + 'sapi' => 'SAPI backend' + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Common constructor. + * + * @access public + */ + function PEAR_Command_Common(&$ui, &$config) + { + parent::PEAR(); + $this->config = &$config; + $this->ui = &$ui; + } + + // }}} + + // {{{ getCommands() + + /** + * Return a list of all the commands defined by this class. + * @return array list of commands + * @access public + */ + function getCommands() + { + $ret = array(); + foreach (array_keys($this->commands) as $command) { + $ret[$command] = $this->commands[$command]['summary']; + } + return $ret; + } + + // }}} + // {{{ getShortcuts() + + /** + * Return a list of all the command shortcuts defined by this class. + * @return array shortcut => command + * @access public + */ + function getShortcuts() + { + $ret = array(); + foreach (array_keys($this->commands) as $command) { + if (isset($this->commands[$command]['shortcut'])) { + $ret[$this->commands[$command]['shortcut']] = $command; + } + } + return $ret; + } + + // }}} + // {{{ getOptions() + + function getOptions($command) + { + $shortcuts = $this->getShortcuts(); + if (isset($shortcuts[$command])) { + $command = $shortcuts[$command]; + } + if (isset($this->commands[$command]) && + isset($this->commands[$command]['options'])) { + return $this->commands[$command]['options']; + } else { + return null; + } + } + + // }}} + // {{{ getGetoptArgs() + + function getGetoptArgs($command, &$short_args, &$long_args) + { + $short_args = ""; + $long_args = array(); + if (empty($this->commands[$command]) || empty($this->commands[$command]['options'])) { + return; + } + reset($this->commands[$command]['options']); + while (list($option, $info) = each($this->commands[$command]['options'])) { + $larg = $sarg = ''; + if (isset($info['arg'])) { + if ($info['arg']{0} == '(') { + $larg = '=='; + $sarg = '::'; + $arg = substr($info['arg'], 1, -1); + } else { + $larg = '='; + $sarg = ':'; + $arg = $info['arg']; + } + } + if (isset($info['shortopt'])) { + $short_args .= $info['shortopt'] . $sarg; + } + $long_args[] = $option . $larg; + } + } + + // }}} + // {{{ getHelp() + /** + * Returns the help message for the given command + * + * @param string $command The command + * @return mixed A fail string if the command does not have help or + * a two elements array containing [0]=>help string, + * [1]=> help string for the accepted cmd args + */ + function getHelp($command) + { + $config = &PEAR_Config::singleton(); + if (!isset($this->commands[$command])) { + return "No such command \"$command\""; + } + $help = null; + if (isset($this->commands[$command]['doc'])) { + $help = $this->commands[$command]['doc']; + } + if (empty($help)) { + // XXX (cox) Fallback to summary if there is no doc (show both?) + if (!isset($this->commands[$command]['summary'])) { + return "No help for command \"$command\""; + } + $help = $this->commands[$command]['summary']; + } + if (preg_match_all('/{config\s+([^\}]+)}/e', $help, $matches)) { + foreach($matches[0] as $k => $v) { + $help = preg_replace("/$v/", $config->get($matches[1][$k]), $help); + } + } + return array($help, $this->getHelpArgs($command)); + } + + // }}} + // {{{ getHelpArgs() + /** + * Returns the help for the accepted arguments of a command + * + * @param string $command + * @return string The help string + */ + function getHelpArgs($command) + { + if (isset($this->commands[$command]['options']) && + count($this->commands[$command]['options'])) + { + $help = "Options:\n"; + foreach ($this->commands[$command]['options'] as $k => $v) { + if (isset($v['arg'])) { + if ($v['arg'][0] == '(') { + $arg = substr($v['arg'], 1, -1); + $sapp = " [$arg]"; + $lapp = "[=$arg]"; + } else { + $sapp = " $v[arg]"; + $lapp = "=$v[arg]"; + } + } else { + $sapp = $lapp = ""; + } + if (isset($v['shortopt'])) { + $s = $v['shortopt']; + $help .= " -$s$sapp, --$k$lapp\n"; + } else { + $help .= " --$k$lapp\n"; + } + $p = " "; + $doc = rtrim(str_replace("\n", "\n$p", $v['doc'])); + $help .= " $doc\n"; + } + return $help; + } + return null; + } + + // }}} + // {{{ run() + + function run($command, $options, $params) + { + if (empty($this->commands[$command]['function'])) { + // look for shortcuts + foreach (array_keys($this->commands) as $cmd) { + if (isset($this->commands[$cmd]['shortcut']) && $this->commands[$cmd]['shortcut'] == $command) { + if (empty($this->commands[$cmd]['function'])) { + return $this->raiseError("unknown command `$command'"); + } else { + $func = $this->commands[$cmd]['function']; + } + $command = $cmd; + break; + } + } + } else { + $func = $this->commands[$command]['function']; + } + return $this->$func($command, $options, $params); + } + + // }}} +} + +?> diff --git a/PEAR/Command/Config.php b/PEAR/Command/Config.php new file mode 100644 index 0000000..f28f225 --- /dev/null +++ b/PEAR/Command/Config.php @@ -0,0 +1,420 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Config.php,v 1.53 2007/06/11 05:11:53 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for managing configuration data. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Config extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'config-show' => array( + 'summary' => 'Show All Settings', + 'function' => 'doConfigShow', + 'shortcut' => 'csh', + 'options' => array( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'show configuration variables for another channel', + 'arg' => 'CHAN', + ), +), + 'doc' => '[layer] +Displays all configuration values. An optional argument +may be used to tell which configuration layer to display. Valid +configuration layers are "user", "system" and "default". To display +configurations for different channels, set the default_channel +configuration variable and run config-show again. +', + ), + 'config-get' => array( + 'summary' => 'Show One Setting', + 'function' => 'doConfigGet', + 'shortcut' => 'cg', + 'options' => array( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'show configuration variables for another channel', + 'arg' => 'CHAN', + ), +), + 'doc' => ' [layer] +Displays the value of one configuration parameter. The +first argument is the name of the parameter, an optional second argument +may be used to tell which configuration layer to look in. Valid configuration +layers are "user", "system" and "default". If no layer is specified, a value +will be picked from the first layer that defines the parameter, in the order +just specified. The configuration value will be retrieved for the channel +specified by the default_channel configuration variable. +', + ), + 'config-set' => array( + 'summary' => 'Change Setting', + 'function' => 'doConfigSet', + 'shortcut' => 'cs', + 'options' => array( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'show configuration variables for another channel', + 'arg' => 'CHAN', + ), +), + 'doc' => ' [layer] +Sets the value of one configuration parameter. The first argument is +the name of the parameter, the second argument is the new value. Some +parameters are subject to validation, and the command will fail with +an error message if the new value does not make sense. An optional +third argument may be used to specify in which layer to set the +configuration parameter. The default layer is "user". The +configuration value will be set for the current channel, which +is controlled by the default_channel configuration variable. +', + ), + 'config-help' => array( + 'summary' => 'Show Information About Setting', + 'function' => 'doConfigHelp', + 'shortcut' => 'ch', + 'options' => array(), + 'doc' => '[parameter] +Displays help for a configuration parameter. Without arguments it +displays help for all configuration parameters. +', + ), + 'config-create' => array( + 'summary' => 'Create a Default configuration file', + 'function' => 'doConfigCreate', + 'shortcut' => 'coc', + 'options' => array( + 'windows' => array( + 'shortopt' => 'w', + 'doc' => 'create a config file for a windows install', + ), + ), + 'doc' => ' +Create a default configuration file with all directory configuration +variables set to subdirectories of , and save it as . +This is useful especially for creating a configuration file for a remote +PEAR installation (using the --remoteconfig option of install, upgrade, +and uninstall). +', + ), + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Config constructor. + * + * @access public + */ + function PEAR_Command_Config(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ doConfigShow() + + function doConfigShow($command, $options, $params) + { + if (is_array($params)) { + $layer = isset($params[0]) ? $params[0] : NULL; + } else { + $layer = NULL; + } + + // $params[0] -> the layer + if ($error = $this->_checkLayer($layer)) { + return $this->raiseError("config-show:$error"); + } + $keys = $this->config->getKeys(); + sort($keys); + $channel = isset($options['channel']) ? $options['channel'] : + $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + $data = array('caption' => 'Configuration (channel ' . $channel . '):'); + foreach ($keys as $key) { + $type = $this->config->getType($key); + $value = $this->config->get($key, $layer, $channel); + if ($type == 'password' && $value) { + $value = '********'; + } + if ($value === false) { + $value = 'false'; + } elseif ($value === true) { + $value = 'true'; + } + $data['data'][$this->config->getGroup($key)][] = array($this->config->getPrompt($key) , $key, $value); + } + foreach ($this->config->getLayers() as $layer) { + $data['data']['Config Files'][] = array(ucfirst($layer) . ' Configuration File', 'Filename' , $this->config->getConfFile($layer)); + } + + $this->ui->outputData($data, $command); + return true; + } + + // }}} + // {{{ doConfigGet() + + function doConfigGet($command, $options, $params) + { + if (!is_array($params)) { + $args_cnt = 0; + } else { + $args_cnt = count($params); + } + + switch ($args_cnt) { + case 1: + $config_key = $params[0]; + $layer = NULL; + break; + case 2: + $config_key = $params[0]; + $layer = $params[1]; + if ($error = $this->_checkLayer($layer)) { + return $this->raiseError("config-get:$error"); + } + break; + case 0: + default: + return $this->raiseError("config-get expects 1 or 2 parameters"); + } + + $channel = isset($options['channel']) ? $options['channel'] : $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + + $this->ui->outputData($this->config->get($config_key, $layer, $channel), $command); + + return true; + } + + // }}} + // {{{ doConfigSet() + + function doConfigSet($command, $options, $params) + { + // $param[0] -> a parameter to set + // $param[1] -> the value for the parameter + // $param[2] -> the layer + $failmsg = ''; + if (sizeof($params) < 2 || sizeof($params) > 3) { + $failmsg .= "config-set expects 2 or 3 parameters"; + return PEAR::raiseError($failmsg); + } + if (isset($params[2]) && ($error = $this->_checkLayer($params[2]))) { + $failmsg .= $error; + return PEAR::raiseError("config-set:$failmsg"); + } + $channel = isset($options['channel']) ? $options['channel'] : + $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + if ($params[0] == 'default_channel') { + if (!$reg->channelExists($params[1])) { + return $this->raiseError('Channel "' . $params[1] . '" does not exist'); + } + } + if (count($params) == 2) { + array_push($params, 'user'); + $layer = 'user'; + } else { + $layer = $params[2]; + } + array_push($params, $channel); + if (!call_user_func_array(array(&$this->config, 'set'), $params)) + { + array_pop($params); + $failmsg = "config-set (" . implode(", ", $params) . ") failed, channel $channel"; + } else { + $this->config->store($layer); + } + if ($failmsg) { + return $this->raiseError($failmsg); + } + $this->ui->outputData('config-set succeeded', $command); + return true; + } + + // }}} + // {{{ doConfigHelp() + + function doConfigHelp($command, $options, $params) + { + if (empty($params)) { + $params = $this->config->getKeys(); + } + $data['caption'] = "Config help" . ((count($params) == 1) ? " for $params[0]" : ''); + $data['headline'] = array('Name', 'Type', 'Description'); + $data['border'] = true; + foreach ($params as $name) { + $type = $this->config->getType($name); + $docs = $this->config->getDocs($name); + if ($type == 'set') { + $docs = rtrim($docs) . "\nValid set: " . + implode(' ', $this->config->getSetValues($name)); + } + $data['data'][] = array($name, $type, $docs); + } + $this->ui->outputData($data, $command); + } + + // }}} + // {{{ doConfigCreate() + + function doConfigCreate($command, $options, $params) + { + if (count($params) != 2) { + return PEAR::raiseError('config-create: must have 2 parameters, root path and ' . + 'filename to save as'); + } + $root = $params[0]; + // Clean up the DIRECTORY_SEPARATOR mess + $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; + $root = preg_replace(array('!\\\\+!', '!/+!', "!$ds2+!"), + array('/', '/', '/'), + $root); + if ($root{0} != '/') { + if (isset($options['windows'])) { + if (!preg_match('/^[A-Za-z]:/', $root)) { + return PEAR::raiseError('Root directory must be an absolute path beginning ' . + 'with "\\" or "C:\\", was: "' . $root . '"'); + } + } else { + return PEAR::raiseError('Root directory must be an absolute path beginning ' . + 'with "/", was: "' . $root . '"'); + } + } + $windows = isset($options['windows']); + if ($windows) { + $root = str_replace('/', '\\', $root); + } + if (!file_exists($params[1])) { + if (!@touch($params[1])) { + return PEAR::raiseError('Could not create "' . $params[1] . '"'); + } + } + $params[1] = realpath($params[1]); + $config = &new PEAR_Config($params[1], '#no#system#config#', false, false); + if ($root{strlen($root) - 1} == '/') { + $root = substr($root, 0, strlen($root) - 1); + } + $config->noRegistry(); + $config->set('php_dir', $windows ? "$root\\pear\\php" : "$root/pear/php", 'user'); + $config->set('data_dir', $windows ? "$root\\pear\\data" : "$root/pear/data"); + $config->set('ext_dir', $windows ? "$root\\pear\\ext" : "$root/pear/ext"); + $config->set('doc_dir', $windows ? "$root\\pear\\docs" : "$root/pear/docs"); + $config->set('test_dir', $windows ? "$root\\pear\\tests" : "$root/pear/tests"); + $config->set('cache_dir', $windows ? "$root\\pear\\cache" : "$root/pear/cache"); + $config->set('download_dir', $windows ? "$root\\pear\\download" : "$root/pear/download"); + $config->set('temp_dir', $windows ? "$root\\pear\\temp" : "$root/pear/temp"); + $config->set('bin_dir', $windows ? "$root\\pear" : "$root/pear"); + $config->writeConfigFile(); + $this->_showConfig($config); + $this->ui->outputData('Successfully created default configuration file "' . $params[1] . '"', + $command); + } + + // }}} + + function _showConfig(&$config) + { + $params = array('user'); + $keys = $config->getKeys(); + sort($keys); + $channel = 'pear.php.net'; + $data = array('caption' => 'Configuration (channel ' . $channel . '):'); + foreach ($keys as $key) { + $type = $config->getType($key); + $value = $config->get($key, 'user', $channel); + if ($type == 'password' && $value) { + $value = '********'; + } + if ($value === false) { + $value = 'false'; + } elseif ($value === true) { + $value = 'true'; + } + $data['data'][$config->getGroup($key)][] = + array($config->getPrompt($key) , $key, $value); + } + foreach ($config->getLayers() as $layer) { + $data['data']['Config Files'][] = + array(ucfirst($layer) . ' Configuration File', 'Filename' , + $config->getConfFile($layer)); + } + + $this->ui->outputData($data, 'config-show'); + return true; + } + // {{{ _checkLayer() + + /** + * Checks if a layer is defined or not + * + * @param string $layer The layer to search for + * @return mixed False on no error or the error message + */ + function _checkLayer($layer = null) + { + if (!empty($layer) && $layer != 'default') { + $layers = $this->config->getLayers(); + if (!in_array($layer, $layers)) { + return " only the layers: \"" . implode('" or "', $layers) . "\" are supported"; + } + } + return false; + } + + // }}} +} + +?> diff --git a/PEAR/Command/Config.xml b/PEAR/Command/Config.xml new file mode 100644 index 0000000..f64a925 --- /dev/null +++ b/PEAR/Command/Config.xml @@ -0,0 +1,92 @@ + + + Show All Settings + doConfigShow + csh + + + c + show configuration variables for another channel + CHAN + + + [layer] +Displays all configuration values. An optional argument +may be used to tell which configuration layer to display. Valid +configuration layers are "user", "system" and "default". To display +configurations for different channels, set the default_channel +configuration variable and run config-show again. + + + + Show One Setting + doConfigGet + cg + + + c + show configuration variables for another channel + CHAN + + + <parameter> [layer] +Displays the value of one configuration parameter. The +first argument is the name of the parameter, an optional second argument +may be used to tell which configuration layer to look in. Valid configuration +layers are "user", "system" and "default". If no layer is specified, a value +will be picked from the first layer that defines the parameter, in the order +just specified. The configuration value will be retrieved for the channel +specified by the default_channel configuration variable. + + + + Change Setting + doConfigSet + cs + + + c + show configuration variables for another channel + CHAN + + + <parameter> <value> [layer] +Sets the value of one configuration parameter. The first argument is +the name of the parameter, the second argument is the new value. Some +parameters are subject to validation, and the command will fail with +an error message if the new value does not make sense. An optional +third argument may be used to specify in which layer to set the +configuration parameter. The default layer is "user". The +configuration value will be set for the current channel, which +is controlled by the default_channel configuration variable. + + + + Show Information About Setting + doConfigHelp + ch + + [parameter] +Displays help for a configuration parameter. Without arguments it +displays help for all configuration parameters. + + + + Create a Default configuration file + doConfigCreate + coc + + + w + create a config file for a windows install + + + <root path> <filename> +Create a default configuration file with all directory configuration +variables set to subdirectories of <root path>, and save it as <filename>. +This is useful especially for creating a configuration file for a remote +PEAR installation (using the --remoteconfig option of install, upgrade, +and uninstall). + + + \ No newline at end of file diff --git a/PEAR/Command/Install.php b/PEAR/Command/Install.php new file mode 100644 index 0000000..6b31def --- /dev/null +++ b/PEAR/Command/Install.php @@ -0,0 +1,1171 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Install.php,v 1.132 2007/06/11 05:32:14 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for installation or deinstallation/upgrading of + * packages. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Install extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'install' => array( + 'summary' => 'Install Package', + 'function' => 'doInstall', + 'shortcut' => 'i', + 'options' => array( + 'force' => array( + 'shortopt' => 'f', + 'doc' => 'will overwrite newer installed packages', + ), + 'loose' => array( + 'shortopt' => 'l', + 'doc' => 'do not check for recommended dependency version', + ), + 'nodeps' => array( + 'shortopt' => 'n', + 'doc' => 'ignore dependencies, install anyway', + ), + 'register-only' => array( + 'shortopt' => 'r', + 'doc' => 'do not install files, only register the package as installed', + ), + 'soft' => array( + 'shortopt' => 's', + 'doc' => 'soft install, fail silently, or upgrade if already installed', + ), + 'nobuild' => array( + 'shortopt' => 'B', + 'doc' => 'don\'t build C extensions', + ), + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'request uncompressed files when downloading', + ), + 'installroot' => array( + 'shortopt' => 'R', + 'arg' => 'DIR', + 'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT), use packagingroot for RPM', + ), + 'packagingroot' => array( + 'shortopt' => 'P', + 'arg' => 'DIR', + 'doc' => 'root directory used when packaging files, like RPM packaging', + ), + 'ignore-errors' => array( + 'doc' => 'force install even if there were errors', + ), + 'alldeps' => array( + 'shortopt' => 'a', + 'doc' => 'install all required and optional dependencies', + ), + 'onlyreqdeps' => array( + 'shortopt' => 'o', + 'doc' => 'install all required dependencies', + ), + 'offline' => array( + 'shortopt' => 'O', + 'doc' => 'do not attempt to download any urls or contact channels', + ), + 'pretend' => array( + 'shortopt' => 'p', + 'doc' => 'Only list the packages that would be downloaded', + ), + ), + 'doc' => '[channel/] ... +Installs one or more PEAR packages. You can specify a package to +install in four ways: + +"Package-1.0.tgz" : installs from a local file + +"http://example.com/Package-1.0.tgz" : installs from +anywhere on the net. + +"package.xml" : installs the package described in +package.xml. Useful for testing, or for wrapping a PEAR package in +another package manager such as RPM. + +"Package[-version/state][.tar]" : queries your default channel\'s server +({config master_server}) and downloads the newest package with +the preferred quality/state ({config preferred_state}). + +To retrieve Package version 1.1, use "Package-1.1," to retrieve +Package state beta, use "Package-beta." To retrieve an uncompressed +file, append .tar (make sure there is no file by the same name first) + +To download a package from another channel, prefix with the channel name like +"channel/Package" + +More than one package may be specified at once. It is ok to mix these +four ways of specifying packages. +'), + 'upgrade' => array( + 'summary' => 'Upgrade Package', + 'function' => 'doInstall', + 'shortcut' => 'up', + 'options' => array( + 'force' => array( + 'shortopt' => 'f', + 'doc' => 'overwrite newer installed packages', + ), + 'loose' => array( + 'shortopt' => 'l', + 'doc' => 'do not check for recommended dependency version', + ), + 'nodeps' => array( + 'shortopt' => 'n', + 'doc' => 'ignore dependencies, upgrade anyway', + ), + 'register-only' => array( + 'shortopt' => 'r', + 'doc' => 'do not install files, only register the package as upgraded', + ), + 'nobuild' => array( + 'shortopt' => 'B', + 'doc' => 'don\'t build C extensions', + ), + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'request uncompressed files when downloading', + ), + 'installroot' => array( + 'shortopt' => 'R', + 'arg' => 'DIR', + 'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT)', + ), + 'ignore-errors' => array( + 'doc' => 'force install even if there were errors', + ), + 'alldeps' => array( + 'shortopt' => 'a', + 'doc' => 'install all required and optional dependencies', + ), + 'onlyreqdeps' => array( + 'shortopt' => 'o', + 'doc' => 'install all required dependencies', + ), + 'offline' => array( + 'shortopt' => 'O', + 'doc' => 'do not attempt to download any urls or contact channels', + ), + 'pretend' => array( + 'shortopt' => 'p', + 'doc' => 'Only list the packages that would be downloaded', + ), + ), + 'doc' => ' ... +Upgrades one or more PEAR packages. See documentation for the +"install" command for ways to specify a package. + +When upgrading, your package will be updated if the provided new +package has a higher version number (use the -f option if you need to +upgrade anyway). + +More than one package may be specified at once. +'), + 'upgrade-all' => array( + 'summary' => 'Upgrade All Packages', + 'function' => 'doUpgradeAll', + 'shortcut' => 'ua', + 'options' => array( + 'nodeps' => array( + 'shortopt' => 'n', + 'doc' => 'ignore dependencies, upgrade anyway', + ), + 'register-only' => array( + 'shortopt' => 'r', + 'doc' => 'do not install files, only register the package as upgraded', + ), + 'nobuild' => array( + 'shortopt' => 'B', + 'doc' => 'don\'t build C extensions', + ), + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'request uncompressed files when downloading', + ), + 'installroot' => array( + 'shortopt' => 'R', + 'arg' => 'DIR', + 'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT), use packagingroot for RPM', + ), + 'ignore-errors' => array( + 'doc' => 'force install even if there were errors', + ), + 'loose' => array( + 'doc' => 'do not check for recommended dependency version', + ), + ), + 'doc' => ' +Upgrades all packages that have a newer release available. Upgrades are +done only if there is a release available of the state specified in +"preferred_state" (currently {config preferred_state}), or a state considered +more stable. +'), + 'uninstall' => array( + 'summary' => 'Un-install Package', + 'function' => 'doUninstall', + 'shortcut' => 'un', + 'options' => array( + 'nodeps' => array( + 'shortopt' => 'n', + 'doc' => 'ignore dependencies, uninstall anyway', + ), + 'register-only' => array( + 'shortopt' => 'r', + 'doc' => 'do not remove files, only register the packages as not installed', + ), + 'installroot' => array( + 'shortopt' => 'R', + 'arg' => 'DIR', + 'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT)', + ), + 'ignore-errors' => array( + 'doc' => 'force install even if there were errors', + ), + 'offline' => array( + 'shortopt' => 'O', + 'doc' => 'do not attempt to uninstall remotely', + ), + ), + 'doc' => '[channel/] ... +Uninstalls one or more PEAR packages. More than one package may be +specified at once. Prefix with channel name to uninstall from a +channel not in your default channel ({config default_channel}) +'), + 'bundle' => array( + 'summary' => 'Unpacks a Pecl Package', + 'function' => 'doBundle', + 'shortcut' => 'bun', + 'options' => array( + 'destination' => array( + 'shortopt' => 'd', + 'arg' => 'DIR', + 'doc' => 'Optional destination directory for unpacking (defaults to current path or "ext" if exists)', + ), + 'force' => array( + 'shortopt' => 'f', + 'doc' => 'Force the unpacking even if there were errors in the package', + ), + ), + 'doc' => ' +Unpacks a Pecl Package into the selected location. It will download the +package if needed. +'), + 'run-scripts' => array( + 'summary' => 'Run Post-Install Scripts bundled with a package', + 'function' => 'doRunScripts', + 'shortcut' => 'rs', + 'options' => array( + ), + 'doc' => ' +Run post-installation scripts in package , if any exist. +'), + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Install constructor. + * + * @access public + */ + function PEAR_Command_Install(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + /** + * For unit testing purposes + */ + function &getDownloader(&$ui, $options, &$config) + { + if (!class_exists('PEAR_Downloader')) { + require_once 'PEAR/Downloader.php'; + } + $a = &new PEAR_Downloader($ui, $options, $config); + return $a; + } + + /** + * For unit testing purposes + */ + function &getInstaller(&$ui) + { + if (!class_exists('PEAR_Installer')) { + require_once 'PEAR/Installer.php'; + } + $a = &new PEAR_Installer($ui); + return $a; + } + + function enableExtension($binaries, $type) + { + if (!($phpini = $this->config->get('php_ini', null, 'pear.php.net'))) { + return PEAR::raiseError('configuration option "php_ini" is not set to php.ini location'); + } + $ini = $this->_parseIni($phpini); + if (PEAR::isError($ini)) { + return $ini; + } + $fp = @fopen($phpini, 'wb'); + if (!$fp) { + return PEAR::raiseError('cannot open php.ini "' . $phpini . '" for writing'); + } + $line = 0; + if ($type == 'extsrc' || $type == 'extbin') { + $search = 'extensions'; + $enable = 'extension'; + } else { + $search = 'zend_extensions'; + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_contents(); + ob_end_clean(); + $debug = function_exists('leak') ? '_debug' : ''; + $ts = preg_match('Thread Safety.+enabled', $info) ? '_ts' : ''; + $enable = 'zend_extension' . $debug . $ts; + } + foreach ($ini[$search] as $line => $extension) { + if (in_array($extension, $binaries, true) || in_array( + $ini['extension_dir'] . DIRECTORY_SEPARATOR . $extension, $binaries, true)) { + // already enabled - assume if one is, all are + return true; + } + } + if ($line) { + $newini = array_slice($ini['all'], 0, $line); + } else { + $newini = array(); + } + foreach ($binaries as $binary) { + if ($ini['extension_dir']) { + $binary = basename($binary); + } + $newini[] = $enable . '="' . $binary . '"' . (OS_UNIX ? "\n" : "\r\n"); + } + $newini = array_merge($newini, array_slice($ini['all'], $line)); + foreach ($newini as $line) { + fwrite($fp, $line); + } + fclose($fp); + return true; + } + + function disableExtension($binaries, $type) + { + if (!($phpini = $this->config->get('php_ini', null, 'pear.php.net'))) { + return PEAR::raiseError('configuration option "php_ini" is not set to php.ini location'); + } + $ini = $this->_parseIni($phpini); + if (PEAR::isError($ini)) { + return $ini; + } + $line = 0; + if ($type == 'extsrc' || $type == 'extbin') { + $search = 'extensions'; + $enable = 'extension'; + } else { + $search = 'zend_extensions'; + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_contents(); + ob_end_clean(); + $debug = function_exists('leak') ? '_debug' : ''; + $ts = preg_match('Thread Safety.+enabled', $info) ? '_ts' : ''; + $enable = 'zend_extension' . $debug . $ts; + } + $found = false; + foreach ($ini[$search] as $line => $extension) { + if (in_array($extension, $binaries, true) || in_array( + $ini['extension_dir'] . DIRECTORY_SEPARATOR . $extension, $binaries, true)) { + $found = true; + break; + } + } + if (!$found) { + // not enabled + return true; + } + $fp = @fopen($phpini, 'wb'); + if (!$fp) { + return PEAR::raiseError('cannot open php.ini "' . $phpini . '" for writing'); + } + if ($line) { + $newini = array_slice($ini['all'], 0, $line); + // delete the enable line + $newini = array_merge($newini, array_slice($ini['all'], $line + 1)); + } else { + $newini = array_slice($ini['all'], 1); + } + foreach ($newini as $line) { + fwrite($fp, $line); + } + fclose($fp); + return true; + } + + function _parseIni($filename) + { + if (file_exists($filename)) { + if (filesize($filename) > 300000) { + return PEAR::raiseError('php.ini "' . $filename . '" is too large, aborting'); + } + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_contents(); + ob_end_clean(); + $debug = function_exists('leak') ? '_debug' : ''; + $ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : ''; + $zend_extension_line = 'zend_extension' . $debug . $ts; + $all = @file($filename); + if (!$all) { + return PEAR::raiseError('php.ini "' . $filename .'" could not be read'); + } + $zend_extensions = $extensions = array(); + // assume this is right, but pull from the php.ini if it is found + $extension_dir = ini_get('extension_dir'); + foreach ($all as $linenum => $line) { + $line = trim($line); + if (!$line) { + continue; + } + if ($line[0] == ';') { + continue; + } + if (strtolower(substr($line, 0, 13)) == 'extension_dir') { + $line = trim(substr($line, 13)); + if ($line[0] == '=') { + $x = trim(substr($line, 1)); + $x = explode(';', $x); + $extension_dir = str_replace('"', '', array_shift($x)); + continue; + } + } + if (strtolower(substr($line, 0, 9)) == 'extension') { + $line = trim(substr($line, 9)); + if ($line[0] == '=') { + $x = trim(substr($line, 1)); + $x = explode(';', $x); + $extensions[$linenum] = str_replace('"', '', array_shift($x)); + continue; + } + } + if (strtolower(substr($line, 0, strlen($zend_extension_line))) == + $zend_extension_line) { + $line = trim(substr($line, strlen($zend_extension_line))); + if ($line[0] == '=') { + $x = trim(substr($line, 1)); + $x = explode(';', $x); + $zend_extensions[$linenum] = str_replace('"', '', array_shift($x)); + continue; + } + } + } + return array( + 'extensions' => $extensions, + 'zend_extensions' => $zend_extensions, + 'extension_dir' => $extension_dir, + 'all' => $all, + ); + } else { + return PEAR::raiseError('php.ini "' . $filename . '" does not exist'); + } + } + + // {{{ doInstall() + + function doInstall($command, $options, $params) + { + if (!class_exists('PEAR/PackageFile.php')) { + require_once 'PEAR/PackageFile.php'; + } + if (empty($this->installer)) { + $this->installer = &$this->getInstaller($this->ui); + } + if ($command == 'upgrade' || $command == 'upgrade-all') { + $options['upgrade'] = true; + } else { + $packages = $params; + } + if (isset($options['installroot']) && isset($options['packagingroot'])) { + return $this->raiseError('ERROR: cannot use both --installroot and --packagingroot'); + } + $reg = &$this->config->getRegistry(); + $instreg = &$reg; // instreg used to check if package is installed + if (isset($options['packagingroot']) && !isset($options['upgrade'])) { + $packrootphp_dir = $this->installer->_prependPath( + $this->config->get('php_dir', null, 'pear.php.net'), + $options['packagingroot']); + $instreg = new PEAR_Registry($packrootphp_dir); // other instreg! + + if ($this->config->get('verbose') > 2) { + $this->ui->outputData('using package root: ' . $options['packagingroot']); + } + } + $abstractpackages = array(); + $otherpackages = array(); + // parse params + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + foreach($params as $param) { + if (strpos($param, 'http://') === 0) { + $otherpackages[] = $param; + continue; + } + if (strpos($param, 'channel://') === false && @file_exists($param)) { + if (isset($options['force'])) { + $otherpackages[] = $param; + continue; + } + $pkg = new PEAR_PackageFile($this->config); + $pf = $pkg->fromAnyFile($param, PEAR_VALIDATE_DOWNLOADING); + if (PEAR::isError($pf)) { + $otherpackages[] = $param; + continue; + } + if ($reg->packageExists($pf->getPackage(), $pf->getChannel()) && + version_compare($pf->getVersion(), + $reg->packageInfo($pf->getPackage(), 'version', $pf->getChannel()), + '<=')) { + if ($this->config->get('verbose')) { + $this->ui->outputData('Ignoring installed package ' . + $reg->parsedPackageNameToString( + array('package' => $pf->getPackage(), + 'channel' => $pf->getChannel()), true)); + } + continue; + } + $otherpackages[] = $param; + continue; + } + $e = $reg->parsePackageName($param, $this->config->get('default_channel')); + if (PEAR::isError($e)) { + $otherpackages[] = $param; + } else { + $abstractpackages[] = $e; + } + } + PEAR::staticPopErrorHandling(); + + // if there are any local package .tgz or remote static url, we can't + // filter. The filter only works for abstract packages + if (count($abstractpackages) && !isset($options['force'])) { + // when not being forced, only do necessary upgrades/installs + if (isset($options['upgrade'])) { + $abstractpackages = $this->_filterUptodatePackages($abstractpackages, + $command); + } else { + foreach ($abstractpackages as $i => $package) { + if (isset($package['group'])) { + // do not filter out install groups + continue; + } + if ($instreg->packageExists($package['package'], $package['channel'])) { + if ($this->config->get('verbose')) { + $this->ui->outputData('Ignoring installed package ' . + $reg->parsedPackageNameToString($package, true)); + } + unset($abstractpackages[$i]); + } + } + } + $abstractpackages = + array_map(array($reg, 'parsedPackageNameToString'), $abstractpackages); + } + + $packages = array_merge($abstractpackages, $otherpackages); + if (!count($packages)) { + $this->ui->outputData('Nothing to ' . $command); + return true; + } + + $this->downloader = &$this->getDownloader($this->ui, $options, $this->config); + $errors = array(); + $downloaded = array(); + $downloaded = &$this->downloader->download($packages); + if (PEAR::isError($downloaded)) { + return $this->raiseError($downloaded); + } + $errors = $this->downloader->getErrorMsgs(); + if (count($errors)) { + $err = array(); + $err['data'] = array(); + foreach ($errors as $error) { + $err['data'][] = array($error); + } + $err['headline'] = 'Install Errors'; + $this->ui->outputData($err); + if (!count($downloaded)) { + return $this->raiseError("$command failed"); + } + } + $data = array( + 'headline' => 'Packages that would be Installed' + ); + if (isset($options['pretend'])) { + foreach ($downloaded as $package) { + $data['data'][] = array($reg->parsedPackageNameToString($package->getParsedPackage())); + } + $this->ui->outputData($data, 'pretend'); + return true; + } + $this->installer->setOptions($options); + $this->installer->sortPackagesForInstall($downloaded); + if (PEAR::isError($err = $this->installer->setDownloadedPackages($downloaded))) { + $this->raiseError($err->getMessage()); + return true; + } + $extrainfo = array(); + foreach ($downloaded as $param) { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $info = $this->installer->install($param, $options); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($info)) { + $oldinfo = $info; + $pkg = &$param->getPackageFile(); + if ($info->getCode() != PEAR_INSTALLER_NOBINARY) { + if (!($info = $pkg->installBinary($this->installer))) { + $this->ui->outputData('ERROR: ' .$oldinfo->getMessage()); + continue; + } + // we just installed a different package than requested, + // let's change the param and info so that the rest of this works + $param = $info[0]; + $info = $info[1]; + } + } + if (is_array($info)) { + if ($param->getPackageType() == 'extsrc' || + $param->getPackageType() == 'extbin' || + $param->getPackageType() == 'zendextsrc' || + $param->getPackageType() == 'zendextbin') { + $pkg = &$param->getPackageFile(); + if ($instbin = $pkg->getInstalledBinary()) { + $instpkg = &$instreg->getPackage($instbin, $pkg->getChannel()); + } else { + $instpkg = &$instreg->getPackage($pkg->getPackage(), $pkg->getChannel()); + } + foreach ($instpkg->getFilelist() as $name => $atts) { + $pinfo = pathinfo($atts['installed_as']); + if (!isset($pinfo['extension']) || + in_array($pinfo['extension'], array('c', 'h'))) { + continue; // make sure we don't match php_blah.h + } + if ((strpos($pinfo['basename'], 'php_') === 0 && + $pinfo['extension'] == 'dll') || + // most unices + $pinfo['extension'] == 'so' || + // hp-ux + $pinfo['extension'] == 'sl') { + $binaries[] = array($atts['installed_as'], $pinfo); + break; + } + } + foreach ($binaries as $pinfo) { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $ret = $this->enableExtension(array($pinfo[0]), $param->getPackageType()); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($ret)) { + $extrainfo[] = $ret->getMessage(); + if ($param->getPackageType() == 'extsrc' || + $param->getPackageType() == 'extbin') { + $exttype = 'extension'; + } else { + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_contents(); + ob_end_clean(); + $debug = function_exists('leak') ? '_debug' : ''; + $ts = preg_match('Thread Safety.+enabled', $info) ? '_ts' : ''; + $exttype = 'zend_extension' . $debug . $ts; + } + $extrainfo[] = 'You should add "' . $exttype . '=' . + $pinfo[1]['basename'] . '" to php.ini'; + } else { + $extrainfo[] = 'Extension ' . $instpkg->getProvidesExtension() . + ' enabled in php.ini'; + } + } + } + if ($this->config->get('verbose') > 0) { + $channel = $param->getChannel(); + $label = $reg->parsedPackageNameToString( + array( + 'channel' => $channel, + 'package' => $param->getPackage(), + 'version' => $param->getVersion(), + )); + $out = array('data' => "$command ok: $label"); + if (isset($info['release_warnings'])) { + $out['release_warnings'] = $info['release_warnings']; + } + $this->ui->outputData($out, $command); + if (!isset($options['register-only']) && !isset($options['offline'])) { + if ($this->config->isDefinedLayer('ftp')) { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $info = $this->installer->ftpInstall($param); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($info)) { + $this->ui->outputData($info->getMessage()); + $this->ui->outputData("remote install failed: $label"); + } else { + $this->ui->outputData("remote install ok: $label"); + } + } + } + } + $deps = $param->getDeps(); + if ($deps) { + if (isset($deps['group'])) { + $groups = $deps['group']; + if (!isset($groups[0])) { + $groups = array($groups); + } + foreach ($groups as $group) { + if ($group['attribs']['name'] == 'default') { + // default group is always installed, unless the user + // explicitly chooses to install another group + continue; + } + $extrainfo[] = $param->getPackage() . ': Optional feature ' . + $group['attribs']['name'] . ' available (' . + $group['attribs']['hint'] . ')'; + } + $extrainfo[] = $param->getPackage() . + ': To install optional features use "pear install ' . + $reg->parsedPackageNameToString( + array('package' => $param->getPackage(), + 'channel' => $param->getChannel()), true) . + '#featurename"'; + } + } + $pkg = &$instreg->getPackage($param->getPackage(), $param->getChannel()); + // $pkg may be NULL if install is a 'fake' install via --packagingroot + if (is_object($pkg)) { + $pkg->setConfig($this->config); + if ($list = $pkg->listPostinstallScripts()) { + $pn = $reg->parsedPackageNameToString(array('channel' => + $param->getChannel(), 'package' => $param->getPackage()), true); + $extrainfo[] = $pn . ' has post-install scripts:'; + foreach ($list as $file) { + $extrainfo[] = $file; + } + $extrainfo[] = $param->getPackage() . + ': Use "pear run-scripts ' . $pn . '" to finish setup.'; + $extrainfo[] = 'DO NOT RUN SCRIPTS FROM UNTRUSTED SOURCES'; + } + } + } else { + return $this->raiseError("$command failed"); + } + } + if (count($extrainfo)) { + foreach ($extrainfo as $info) { + $this->ui->outputData($info); + } + } + return true; + } + + // }}} + // {{{ doUpgradeAll() + + function doUpgradeAll($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $toUpgrade = array(); + foreach ($reg->listChannels() as $channel) { + if ($channel == '__uri') { + continue; + } + + // parse name with channel + foreach ($reg->listPackages($channel) as $name) { + $toUpgrade[] = $reg->parsedPackageNameToString(array( + 'channel' => $channel, + 'package' => $name + )); + } + } + + $err = $this->doInstall('upgrade-all', $options, $toUpgrade); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage(), $command); + } + } + + // }}} + // {{{ doUninstall() + + function doUninstall($command, $options, $params) + { + if (empty($this->installer)) { + $this->installer = &$this->getInstaller($this->ui); + } + if (isset($options['remoteconfig'])) { + $e = $this->config->readFTPConfigFile($options['remoteconfig']); + if (!PEAR::isError($e)) { + $this->installer->setConfig($this->config); + } + } + if (sizeof($params) < 1) { + return $this->raiseError("Please supply the package(s) you want to uninstall"); + } + $reg = &$this->config->getRegistry(); + $newparams = array(); + $badparams = array(); + foreach ($params as $pkg) { + $channel = $this->config->get('default_channel'); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $parsed = $reg->parsePackageName($pkg, $channel); + PEAR::staticPopErrorHandling(); + if (!$parsed || PEAR::isError($parsed)) { + $badparams[] = $pkg; + continue; + } + $package = $parsed['package']; + $channel = $parsed['channel']; + $info = &$reg->getPackage($package, $channel); + if ($info === null && + ($channel == 'pear.php.net' || $channel == 'pecl.php.net')) { + // make sure this isn't a package that has flipped from pear to pecl but + // used a package.xml 1.0 + $testc = ($channel == 'pear.php.net') ? 'pecl.php.net' : 'pear.php.net'; + $info = &$reg->getPackage($package, $testc); + if ($info !== null) { + $channel = $testc; + } + } + if ($info === null) { + $badparams[] = $pkg; + } else { + $newparams[] = &$info; + // check for binary packages (this is an alias for those packages if so) + if ($installedbinary = $info->getInstalledBinary()) { + $this->ui->log('adding binary package ' . + $reg->parsedPackageNameToString(array('channel' => $channel, + 'package' => $installedbinary), true)); + $newparams[] = &$reg->getPackage($installedbinary, $channel); + } + // add the contents of a dependency group to the list of installed packages + if (isset($parsed['group'])) { + $group = $info->getDependencyGroup($parsed['group']); + if ($group) { + $installed = &$reg->getInstalledGroup($group); + if ($installed) { + foreach ($installed as $i => $p) { + $newparams[] = &$installed[$i]; + } + } + } + } + } + } + $err = $this->installer->sortPackagesForUninstall($newparams); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage(), $command); + return true; + } + $params = $newparams; + // twist this to use it to check on whether dependent packages are also being uninstalled + // for circular dependencies like subpackages + $this->installer->setUninstallPackages($newparams); + $params = array_merge($params, $badparams); + foreach ($params as $pkg) { + $this->installer->pushErrorHandling(PEAR_ERROR_RETURN); + if ($err = $this->installer->uninstall($pkg, $options)) { + $this->installer->popErrorHandling(); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage(), $command); + continue; + } + if ($pkg->getPackageType() == 'extsrc' || + $pkg->getPackageType() == 'extbin' || + $pkg->getPackageType() == 'zendextsrc' || + $pkg->getPackageType() == 'zendextbin') { + if ($instbin = $pkg->getInstalledBinary()) { + continue; // this will be uninstalled later + } + foreach ($pkg->getFilelist() as $name => $atts) { + $pinfo = pathinfo($atts['installed_as']); + if (!isset($pinfo['extension']) || + in_array($pinfo['extension'], array('c', 'h'))) { + continue; // make sure we don't match php_blah.h + } + if ((strpos($pinfo['basename'], 'php_') === 0 && + $pinfo['extension'] == 'dll') || + // most unices + $pinfo['extension'] == 'so' || + // hp-ux + $pinfo['extension'] == 'sl') { + $binaries[] = array($atts['installed_as'], $pinfo); + break; + } + } + foreach ($binaries as $pinfo) { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $ret = $this->disableExtension(array($pinfo[0]), $pkg->getPackageType()); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($ret)) { + $extrainfo[] = $ret->getMessage(); + if ($pkg->getPackageType() == 'extsrc' || + $pkg->getPackageType() == 'extbin') { + $exttype = 'extension'; + } else { + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_contents(); + ob_end_clean(); + $debug = function_exists('leak') ? '_debug' : ''; + $ts = preg_match('Thread Safety.+enabled', $info) ? '_ts' : ''; + $exttype = 'zend_extension' . $debug . $ts; + } + $this->ui->outputData('Unable to remove "' . $exttype . '=' . + $pinfo[1]['basename'] . '" from php.ini', $command); + } else { + $this->ui->outputData('Extension ' . $pkg->getProvidesExtension() . + ' disabled in php.ini', $command); + } + } + } + $savepkg = $pkg; + if ($this->config->get('verbose') > 0) { + if (is_object($pkg)) { + $pkg = $reg->parsedPackageNameToString($pkg); + } + $this->ui->outputData("uninstall ok: $pkg", $command); + } + if (!isset($options['offline']) && is_object($savepkg) && + defined('PEAR_REMOTEINSTALL_OK')) { + if ($this->config->isDefinedLayer('ftp')) { + $this->installer->pushErrorHandling(PEAR_ERROR_RETURN); + $info = $this->installer->ftpUninstall($savepkg); + $this->installer->popErrorHandling(); + if (PEAR::isError($info)) { + $this->ui->outputData($info->getMessage()); + $this->ui->outputData("remote uninstall failed: $pkg"); + } else { + $this->ui->outputData("remote uninstall ok: $pkg"); + } + } + } + } else { + $this->installer->popErrorHandling(); + if (is_object($pkg)) { + $pkg = $reg->parsedPackageNameToString($pkg); + } + return $this->raiseError("uninstall failed: $pkg"); + } + } + return true; + } + + // }}} + + + // }}} + // {{{ doBundle() + /* + (cox) It just downloads and untars the package, does not do + any check that the PEAR_Installer::_installFile() does. + */ + + function doBundle($command, $options, $params) + { + $downloader = &$this->getDownloader($this->ui, array('force' => true, 'nodeps' => true, + 'soft' => true, 'downloadonly' => true), $this->config); + $reg = &$this->config->getRegistry(); + if (sizeof($params) < 1) { + return $this->raiseError("Please supply the package you want to bundle"); + } + + if (isset($options['destination'])) { + if (!is_dir($options['destination'])) { + System::mkdir('-p ' . $options['destination']); + } + $dest = realpath($options['destination']); + } else { + $pwd = getcwd(); + if (is_dir($pwd . DIRECTORY_SEPARATOR . 'ext')) { + $dest = $pwd . DIRECTORY_SEPARATOR . 'ext'; + } else { + $dest = $pwd; + } + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = $downloader->setDownloadDir($dest); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($err)) { + return PEAR::raiseError('download directory "' . $dest . + '" is not writeable.'); + } + $result = &$downloader->download(array($params[0])); + if (PEAR::isError($result)) { + return $result; + } + $pkgfile = &$result[0]->getPackageFile(); + $pkgname = $pkgfile->getName(); + $pkgversion = $pkgfile->getVersion(); + + // Unpacking ------------------------------------------------- + $dest .= DIRECTORY_SEPARATOR . $pkgname; + $orig = $pkgname . '-' . $pkgversion; + + $tar = &new Archive_Tar($pkgfile->getArchiveFile()); + if (!$tar->extractModify($dest, $orig)) { + return $this->raiseError('unable to unpack ' . $pkgfile->getArchiveFile()); + } + $this->ui->outputData("Package ready at '$dest'"); + // }}} + } + + // }}} + + function doRunScripts($command, $options, $params) + { + if (!isset($params[0])) { + return $this->raiseError('run-scripts expects 1 parameter: a package name'); + } + $reg = &$this->config->getRegistry(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $parsed = $reg->parsePackageName($params[0], $this->config->get('default_channel')); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($parsed)) { + return $this->raiseError($parsed); + } + $package = &$reg->getPackage($parsed['package'], $parsed['channel']); + if (is_object($package)) { + $package->setConfig($this->config); + $package->runPostinstallScripts(); + } else { + return $this->raiseError('Could not retrieve package "' . $params[0] . '" from registry'); + } + $this->ui->outputData('Install scripts complete', $command); + return true; + } + + /** + * Given a list of packages, filter out those ones that are already up to date + * + * @param $packages: packages, in parsed array format ! + * @return list of packages that can be upgraded + */ + function _filterUptodatePackages($packages, $command) + { + $reg = &$this->config->getRegistry(); + $latestReleases = array(); + + $ret = array(); + foreach($packages as $package) { + if (isset($package['group'])) { + $ret[] = $package; + continue; + } + $channel = $package['channel']; + $name = $package['package']; + + if (!$reg->packageExists($name, $channel)) { + $ret[] = $package; + continue; + } + if (!isset($latestReleases[$channel])) { + // fill in cache for this channel + $chan = &$reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $this->raiseError($chan); + } + if ($chan->supportsREST($this->config->get('preferred_mirror', + null, $channel)) && + $base = $chan->getBaseURL('REST1.0', + $this->config->get('preferred_mirror', + null, $channel))) + { + $dorest = true; + } else { + $dorest = false; + $remote = &$this->config->getRemote($this->config); + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if ($dorest) { + $rest = &$this->config->getREST('1.0', array()); + $installed = array_flip($reg->listPackages($channel)); + $latest = $rest->listLatestUpgrades($base, + $this->config->get('preferred_state', null, $channel), $installed, + $channel, $reg); + } else { + $latest = $remote->call("package.listLatestReleases", + $this->config->get('preferred_state', null, $channel)); + unset($remote); + } + PEAR::staticPopErrorHandling(); + if (PEAR::isError($latest)) { + $this->ui->outputData('Error getting channel info from ' . $channel . + ': ' . $latest->getMessage()); + continue; + } + + $latestReleases[$channel] = array_change_key_case($latest); + } + + // check package for latest release + if (isset($latestReleases[$channel][strtolower($name)])) { + // if not set, up to date + $inst_version = $reg->packageInfo($name, 'version', $channel); + $channel_version = $latestReleases[$channel][strtolower($name)]['version']; + if (version_compare($channel_version, $inst_version, "le")) { + // installed version is up-to-date + continue; + } + // maintain BC + if ($command == 'upgrade-all') { + $this->ui->outputData(array('data' => 'Will upgrade ' . + $reg->parsedPackageNameToString($package)), $command); + } + $ret[] = $package; + } + } + + return $ret; + } + +} +?> diff --git a/PEAR/Command/Install.xml b/PEAR/Command/Install.xml new file mode 100644 index 0000000..94044c2 --- /dev/null +++ b/PEAR/Command/Install.xml @@ -0,0 +1,259 @@ + + + Install Package + doInstall + i + + + f + will overwrite newer installed packages + + + l + do not check for recommended dependency version + + + n + ignore dependencies, install anyway + + + r + do not install files, only register the package as installed + + + s + soft install, fail silently, or upgrade if already installed + + + B + don't build C extensions + + + Z + request uncompressed files when downloading + + + R + DIR + root directory used when installing files (ala PHP's INSTALL_ROOT), use packagingroot for RPM + + + P + DIR + root directory used when packaging files, like RPM packaging + + + force install even if there were errors + + + a + install all required and optional dependencies + + + o + install all required dependencies + + + O + do not attempt to download any urls or contact channels + + + p + Only list the packages that would be downloaded + + + [channel/]<package> ... +Installs one or more PEAR packages. You can specify a package to +install in four ways: + +"Package-1.0.tgz" : installs from a local file + +"http://example.com/Package-1.0.tgz" : installs from +anywhere on the net. + +"package.xml" : installs the package described in +package.xml. Useful for testing, or for wrapping a PEAR package in +another package manager such as RPM. + +"Package[-version/state][.tar]" : queries your default channel's server +({config master_server}) and downloads the newest package with +the preferred quality/state ({config preferred_state}). + +To retrieve Package version 1.1, use "Package-1.1," to retrieve +Package state beta, use "Package-beta." To retrieve an uncompressed +file, append .tar (make sure there is no file by the same name first) + +To download a package from another channel, prefix with the channel name like +"channel/Package" + +More than one package may be specified at once. It is ok to mix these +four ways of specifying packages. + + + + Upgrade Package + doInstall + up + + + f + overwrite newer installed packages + + + l + do not check for recommended dependency version + + + n + ignore dependencies, upgrade anyway + + + r + do not install files, only register the package as upgraded + + + B + don't build C extensions + + + Z + request uncompressed files when downloading + + + R + DIR + root directory used when installing files (ala PHP's INSTALL_ROOT) + + + force install even if there were errors + + + a + install all required and optional dependencies + + + o + install all required dependencies + + + O + do not attempt to download any urls or contact channels + + + p + Only list the packages that would be downloaded + + + <package> ... +Upgrades one or more PEAR packages. See documentation for the +"install" command for ways to specify a package. + +When upgrading, your package will be updated if the provided new +package has a higher version number (use the -f option if you need to +upgrade anyway). + +More than one package may be specified at once. + + + + Upgrade All Packages + doInstall + ua + + + n + ignore dependencies, upgrade anyway + + + r + do not install files, only register the package as upgraded + + + B + don't build C extensions + + + Z + request uncompressed files when downloading + + + R + DIR + root directory used when installing files (ala PHP's INSTALL_ROOT) + + + force install even if there were errors + + + do not check for recommended dependency version + + + +Upgrades all packages that have a newer release available. Upgrades are +done only if there is a release available of the state specified in +"preferred_state" (currently {config preferred_state}), or a state considered +more stable. + + + + Un-install Package + doUninstall + un + + + n + ignore dependencies, uninstall anyway + + + r + do not remove files, only register the packages as not installed + + + R + DIR + root directory used when installing files (ala PHP's INSTALL_ROOT) + + + force install even if there were errors + + + O + do not attempt to uninstall remotely + + + [channel/]<package> ... +Uninstalls one or more PEAR packages. More than one package may be +specified at once. Prefix with channel name to uninstall from a +channel not in your default channel ({config default_channel}) + + + + Unpacks a Pecl Package + doBundle + bun + + + d + DIR + Optional destination directory for unpacking (defaults to current path or "ext" if exists) + + + f + Force the unpacking even if there were errors in the package + + + <package> +Unpacks a Pecl Package into the selected location. It will download the +package if needed. + + + + Run Post-Install Scripts bundled with a package + doRunScripts + rs + + <package> +Run post-installation scripts in package <package>, if any exist. + + + \ No newline at end of file diff --git a/PEAR/Command/Mirror.php b/PEAR/Command/Mirror.php new file mode 100644 index 0000000..2d98e41 --- /dev/null +++ b/PEAR/Command/Mirror.php @@ -0,0 +1,153 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Mirror.php,v 1.18 2006/03/02 18:14:13 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.2.0 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for providing file mirrors + * + * @category pear + * @package PEAR + * @author Alexander Merz + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.2.0 + */ +class PEAR_Command_Mirror extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'download-all' => array( + 'summary' => 'Downloads each available package from the default channel', + 'function' => 'doDownloadAll', + 'shortcut' => 'da', + 'options' => array( + 'channel' => + array( + 'shortopt' => 'c', + 'doc' => 'specify a channel other than the default channel', + 'arg' => 'CHAN', + ), + ), + 'doc' => ' +Requests a list of available packages from the default channel ({config default_channel}) +and downloads them to current working directory. Note: only +packages within preferred_state ({config preferred_state}) will be downloaded' + ), + ); + + // }}} + + // {{{ constructor + + /** + * PEAR_Command_Mirror constructor. + * + * @access public + * @param object PEAR_Frontend a reference to an frontend + * @param object PEAR_Config a reference to the configuration data + */ + function PEAR_Command_Mirror(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + /** + * For unit-testing + */ + function &factory($a) + { + $a = &PEAR_Command::factory($a, $this->config); + return $a; + } + + // {{{ doDownloadAll() + /** + * retrieves a list of avaible Packages from master server + * and downloads them + * + * @access public + * @param string $command the command + * @param array $options the command options before the command + * @param array $params the stuff after the command name + * @return bool true if succesful + * @throw PEAR_Error + */ + function doDownloadAll($command, $options, $params) + { + $savechannel = $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + $channel = isset($options['channel']) ? $options['channel'] : + $this->config->get('default_channel'); + if (!$reg->channelExists($channel)) { + $this->config->set('default_channel', $savechannel); + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + $this->config->set('default_channel', $channel); + $this->ui->outputData('Using Channel ' . $this->config->get('default_channel')); + $chan = $reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $this->raiseError($chan); + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + $remoteInfo = array_flip($rest->listPackages($base)); + } else { + $remote = &$this->config->getRemote(); + $stable = ($this->config->get('preferred_state') == 'stable'); + $remoteInfo = $remote->call("package.listAll", true, $stable, false); + } + if (PEAR::isError($remoteInfo)) { + return $remoteInfo; + } + $cmd = &$this->factory("download"); + if (PEAR::isError($cmd)) { + return $cmd; + } + $this->ui->outputData('Using Preferred State of ' . + $this->config->get('preferred_state')); + $this->ui->outputData('Gathering release information, please wait...'); + /** + * Error handling not necessary, because already done by + * the download command + */ + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = $cmd->run('download', array('downloadonly' => true), array_keys($remoteInfo)); + PEAR::staticPopErrorHandling(); + $this->config->set('default_channel', $savechannel); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage()); + } + return true; + } + + // }}} +} diff --git a/PEAR/Command/Mirror.xml b/PEAR/Command/Mirror.xml new file mode 100644 index 0000000..fe8be9d --- /dev/null +++ b/PEAR/Command/Mirror.xml @@ -0,0 +1,18 @@ + + + Downloads each available package from the default channel + doDownloadAll + da + + + c + specify a channel other than the default channel + CHAN + + + +Requests a list of available packages from the default channel ({config default_channel}) +and downloads them to current working directory. Note: only +packages within preferred_state ({config preferred_state}) will be downloaded + + \ No newline at end of file diff --git a/PEAR/Command/Package.php b/PEAR/Command/Package.php new file mode 100644 index 0000000..731ed91 --- /dev/null +++ b/PEAR/Command/Package.php @@ -0,0 +1,844 @@ + + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Package.php,v 1.124 2007/04/19 03:04:16 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for login/logout + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ + +class PEAR_Command_Package extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'package' => array( + 'summary' => 'Build Package', + 'function' => 'doPackage', + 'shortcut' => 'p', + 'options' => array( + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'Do not gzip the package file' + ), + 'showname' => array( + 'shortopt' => 'n', + 'doc' => 'Print the name of the packaged file.', + ), + ), + 'doc' => '[descfile] [descfile2] +Creates a PEAR package from its description file (usually called +package.xml). If a second packagefile is passed in, then +the packager will check to make sure that one is a package.xml +version 1.0, and the other is a package.xml version 2.0. The +package.xml version 1.0 will be saved as "package.xml" in the archive, +and the other as "package2.xml" in the archive" +' + ), + 'package-validate' => array( + 'summary' => 'Validate Package Consistency', + 'function' => 'doPackageValidate', + 'shortcut' => 'pv', + 'options' => array(), + 'doc' => ' +', + ), + 'cvsdiff' => array( + 'summary' => 'Run a "cvs diff" for all files in a package', + 'function' => 'doCvsDiff', + 'shortcut' => 'cd', + 'options' => array( + 'quiet' => array( + 'shortopt' => 'q', + 'doc' => 'Be quiet', + ), + 'reallyquiet' => array( + 'shortopt' => 'Q', + 'doc' => 'Be really quiet', + ), + 'date' => array( + 'shortopt' => 'D', + 'doc' => 'Diff against revision of DATE', + 'arg' => 'DATE', + ), + 'release' => array( + 'shortopt' => 'R', + 'doc' => 'Diff against tag for package release REL', + 'arg' => 'REL', + ), + 'revision' => array( + 'shortopt' => 'r', + 'doc' => 'Diff against revision REV', + 'arg' => 'REV', + ), + 'context' => array( + 'shortopt' => 'c', + 'doc' => 'Generate context diff', + ), + 'unified' => array( + 'shortopt' => 'u', + 'doc' => 'Generate unified diff', + ), + 'ignore-case' => array( + 'shortopt' => 'i', + 'doc' => 'Ignore case, consider upper- and lower-case letters equivalent', + ), + 'ignore-whitespace' => array( + 'shortopt' => 'b', + 'doc' => 'Ignore changes in amount of white space', + ), + 'ignore-blank-lines' => array( + 'shortopt' => 'B', + 'doc' => 'Ignore changes that insert or delete blank lines', + ), + 'brief' => array( + 'doc' => 'Report only whether the files differ, no details', + ), + 'dry-run' => array( + 'shortopt' => 'n', + 'doc' => 'Don\'t do anything, just pretend', + ), + ), + 'doc' => ' +Compares all the files in a package. Without any options, this +command will compare the current code with the last checked-in code. +Using the -r or -R option you may compare the current code with that +of a specific release. +', + ), + 'cvstag' => array( + 'summary' => 'Set CVS Release Tag', + 'function' => 'doCvsTag', + 'shortcut' => 'ct', + 'options' => array( + 'quiet' => array( + 'shortopt' => 'q', + 'doc' => 'Be quiet', + ), + 'reallyquiet' => array( + 'shortopt' => 'Q', + 'doc' => 'Be really quiet', + ), + 'slide' => array( + 'shortopt' => 'F', + 'doc' => 'Move (slide) tag if it exists', + ), + 'delete' => array( + 'shortopt' => 'd', + 'doc' => 'Remove tag', + ), + 'dry-run' => array( + 'shortopt' => 'n', + 'doc' => 'Don\'t do anything, just pretend', + ), + ), + 'doc' => ' [files...] +Sets a CVS tag on all files in a package. Use this command after you have +packaged a distribution tarball with the "package" command to tag what +revisions of what files were in that release. If need to fix something +after running cvstag once, but before the tarball is released to the public, +use the "slide" option to move the release tag. + +to include files (such as a second package.xml, or tests not included in the +release), pass them as additional parameters. +', + ), + 'package-dependencies' => array( + 'summary' => 'Show package dependencies', + 'function' => 'doPackageDependencies', + 'shortcut' => 'pd', + 'options' => array(), + 'doc' => ' +List all dependencies the package has.' + ), + 'sign' => array( + 'summary' => 'Sign a package distribution file', + 'function' => 'doSign', + 'shortcut' => 'si', + 'options' => array( + 'verbose' => array( + 'shortopt' => 'v', + 'doc' => 'Display GnuPG output', + ), + ), + 'doc' => ' +Signs a package distribution (.tar or .tgz) file with GnuPG.', + ), + 'makerpm' => array( + 'summary' => 'Builds an RPM spec file from a PEAR package', + 'function' => 'doMakeRPM', + 'shortcut' => 'rpm', + 'options' => array( + 'spec-template' => array( + 'shortopt' => 't', + 'arg' => 'FILE', + 'doc' => 'Use FILE as RPM spec file template' + ), + 'rpm-pkgname' => array( + 'shortopt' => 'p', + 'arg' => 'FORMAT', + 'doc' => 'Use FORMAT as format string for RPM package name, %s is replaced +by the PEAR package name, defaults to "PEAR::%s".', + ), + ), + 'doc' => ' + +Creates an RPM .spec file for wrapping a PEAR package inside an RPM +package. Intended to be used from the SPECS directory, with the PEAR +package tarball in the SOURCES directory: + +$ pear makerpm ../SOURCES/Net_Socket-1.0.tgz +Wrote RPM spec file PEAR::Net_Geo-1.0.spec +$ rpm -bb PEAR::Net_Socket-1.0.spec +... +Wrote: /usr/src/redhat/RPMS/i386/PEAR::Net_Socket-1.0-1.i386.rpm +', + ), + 'convert' => array( + 'summary' => 'Convert a package.xml 1.0 to package.xml 2.0 format', + 'function' => 'doConvert', + 'shortcut' => 'c2', + 'options' => array( + 'flat' => array( + 'shortopt' => 'f', + 'doc' => 'do not beautify the filelist.', + ), + ), + 'doc' => '[descfile] [descfile2] +Converts a package.xml in 1.0 format into a package.xml +in 2.0 format. The new file will be named package2.xml by default, +and package.xml will be used as the old file by default. +This is not the most intelligent conversion, and should only be +used for automated conversion or learning the format. +' + ), + ); + + var $output; + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Package constructor. + * + * @access public + */ + function PEAR_Command_Package(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ _displayValidationResults() + + function _displayValidationResults($err, $warn, $strict = false) + { + foreach ($err as $e) { + $this->output .= "Error: $e\n"; + } + foreach ($warn as $w) { + $this->output .= "Warning: $w\n"; + } + $this->output .= sprintf('Validation: %d error(s), %d warning(s)'."\n", + sizeof($err), sizeof($warn)); + if ($strict && sizeof($err) > 0) { + $this->output .= "Fix these errors and try again."; + return false; + } + return true; + } + + // }}} + function &getPackager() + { + if (!class_exists('PEAR_Packager')) { + require_once 'PEAR/Packager.php'; + } + $a = &new PEAR_Packager; + return $a; + } + + function &getPackageFile($config, $debug = false, $tmpdir = null) + { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (!class_exists('PEAR/PackageFile.php')) { + require_once 'PEAR/PackageFile.php'; + } + $a = &new PEAR_PackageFile($config, $debug, $tmpdir); + $common = new PEAR_Common; + $common->ui = $this->ui; + $a->setLogger($common); + return $a; + } + // {{{ doPackage() + + function doPackage($command, $options, $params) + { + $this->output = ''; + $pkginfofile = isset($params[0]) ? $params[0] : 'package.xml'; + $pkg2 = isset($params[1]) ? $params[1] : null; + if (!$pkg2 && !isset($params[0])) { + if (file_exists('package2.xml')) { + $pkg2 = 'package2.xml'; + } + } + $packager = &$this->getPackager(); + $compress = empty($options['nocompress']) ? true : false; + $result = $packager->package($pkginfofile, $compress, $pkg2); + if (PEAR::isError($result)) { + return $this->raiseError($result); + } + // Don't want output, only the package file name just created + if (isset($options['showname'])) { + $this->output = $result; + } + if ($this->output) { + $this->ui->outputData($this->output, $command); + } + return true; + } + + // }}} + // {{{ doPackageValidate() + + function doPackageValidate($command, $options, $params) + { + $this->output = ''; + if (sizeof($params) < 1) { + $params[0] = "package.xml"; + } + $obj = &$this->getPackageFile($this->config, $this->_debug); + $obj->rawReturn(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $info = $obj->fromTgzFile($params[0], PEAR_VALIDATE_NORMAL); + if (PEAR::isError($info)) { + $info = $obj->fromPackageFile($params[0], PEAR_VALIDATE_NORMAL); + } else { + $archive = $info->getArchiveFile(); + $tar = &new Archive_Tar($archive); + $tar->extract(dirname($info->getPackageFile())); + $info->setPackageFile(dirname($info->getPackageFile()) . DIRECTORY_SEPARATOR . + $info->getPackage() . '-' . $info->getVersion() . DIRECTORY_SEPARATOR . + basename($info->getPackageFile())); + } + PEAR::staticPopErrorHandling(); + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + $valid = false; + if ($info->getPackagexmlVersion() == '2.0') { + if ($valid = $info->validate(PEAR_VALIDATE_NORMAL)) { + $info->flattenFileList(); + $valid = $info->validate(PEAR_VALIDATE_PACKAGING); + } + } else { + $valid = $info->validate(PEAR_VALIDATE_PACKAGING); + } + $err = array(); + $warn = array(); + if (!$valid) { + foreach ($info->getValidationWarnings() as $error) { + if ($error['level'] == 'warning') { + $warn[] = $error['message']; + } else { + $err[] = $error['message']; + } + } + } + $this->_displayValidationResults($err, $warn); + $this->ui->outputData($this->output, $command); + return true; + } + + // }}} + // {{{ doCvsTag() + + function doCvsTag($command, $options, $params) + { + $this->output = ''; + $_cmd = $command; + if (sizeof($params) < 1) { + $help = $this->getHelp($command); + return $this->raiseError("$command: missing parameter: $help[0]"); + } + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + $err = $warn = array(); + if (!$info->validate()) { + foreach ($info->getValidationWarnings() as $error) { + if ($error['level'] == 'warning') { + $warn[] = $error['message']; + } else { + $err[] = $error['message']; + } + } + } + if (!$this->_displayValidationResults($err, $warn, true)) { + $this->ui->outputData($this->output, $command); + return $this->raiseError('CVS tag failed'); + } + $version = $info->getVersion(); + $cvsversion = preg_replace('/[^a-z0-9]/i', '_', $version); + $cvstag = "RELEASE_$cvsversion"; + $files = array_keys($info->getFilelist()); + $command = "cvs"; + if (isset($options['quiet'])) { + $command .= ' -q'; + } + if (isset($options['reallyquiet'])) { + $command .= ' -Q'; + } + $command .= ' tag'; + if (isset($options['slide'])) { + $command .= ' -F'; + } + if (isset($options['delete'])) { + $command .= ' -d'; + } + $command .= ' ' . $cvstag . ' ' . escapeshellarg($params[0]); + array_shift($params); + if (count($params)) { + // add in additional files to be tagged + $files = array_merge($files, $params); + } + foreach ($files as $file) { + $command .= ' ' . escapeshellarg($file); + } + if ($this->config->get('verbose') > 1) { + $this->output .= "+ $command\n"; + } + $this->output .= "+ $command\n"; + if (empty($options['dry-run'])) { + $fp = popen($command, "r"); + while ($line = fgets($fp, 1024)) { + $this->output .= rtrim($line)."\n"; + } + pclose($fp); + } + $this->ui->outputData($this->output, $_cmd); + return true; + } + + // }}} + // {{{ doCvsDiff() + + function doCvsDiff($command, $options, $params) + { + $this->output = ''; + if (sizeof($params) < 1) { + $help = $this->getHelp($command); + return $this->raiseError("$command: missing parameter: $help[0]"); + } + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + $err = $warn = array(); + if (!$info->validate()) { + foreach ($info->getValidationWarnings() as $error) { + if ($error['level'] == 'warning') { + $warn[] = $error['message']; + } else { + $err[] = $error['message']; + } + } + } + if (!$this->_displayValidationResults($err, $warn, true)) { + $this->ui->outputData($this->output, $command); + return $this->raiseError('CVS diff failed'); + } + $info1 = $info->getFilelist(); + $files = $info1; + $cmd = "cvs"; + if (isset($options['quiet'])) { + $cmd .= ' -q'; + unset($options['quiet']); + } + if (isset($options['reallyquiet'])) { + $cmd .= ' -Q'; + unset($options['reallyquiet']); + } + if (isset($options['release'])) { + $cvsversion = preg_replace('/[^a-z0-9]/i', '_', $options['release']); + $cvstag = "RELEASE_$cvsversion"; + $options['revision'] = $cvstag; + unset($options['release']); + } + $execute = true; + if (isset($options['dry-run'])) { + $execute = false; + unset($options['dry-run']); + } + $cmd .= ' diff'; + // the rest of the options are passed right on to "cvs diff" + foreach ($options as $option => $optarg) { + $arg = $short = false; + if (isset($this->commands[$command]['options'][$option])) { + $arg = $this->commands[$command]['options'][$option]['arg']; + $short = $this->commands[$command]['options'][$option]['shortopt']; + } + $cmd .= $short ? " -$short" : " --$option"; + if ($arg && $optarg) { + $cmd .= ($short ? '' : '=') . escapeshellarg($optarg); + } + } + foreach ($files as $file) { + $cmd .= ' ' . escapeshellarg($file['name']); + } + if ($this->config->get('verbose') > 1) { + $this->output .= "+ $cmd\n"; + } + if ($execute) { + $fp = popen($cmd, "r"); + while ($line = fgets($fp, 1024)) { + $this->output .= rtrim($line)."\n"; + } + pclose($fp); + } + $this->ui->outputData($this->output, $command); + return true; + } + + // }}} + // {{{ doPackageDependencies() + + function doPackageDependencies($command, $options, $params) + { + // $params[0] -> the PEAR package to list its information + if (sizeof($params) != 1) { + return $this->raiseError("bad parameter(s), try \"help $command\""); + } + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + $deps = $info->getDeps(); + if (is_array($deps)) { + if ($info->getPackagexmlVersion() == '1.0') { + $data = array( + 'caption' => 'Dependencies for pear/' . $info->getPackage(), + 'border' => true, + 'headline' => array("Required?", "Type", "Name", "Relation", "Version"), + ); + + foreach ($deps as $d) { + if (isset($d['optional'])) { + if ($d['optional'] == 'yes') { + $req = 'No'; + } else { + $req = 'Yes'; + } + } else { + $req = 'Yes'; + } + if (isset($this->_deps_rel_trans[$d['rel']])) { + $rel = $this->_deps_rel_trans[$d['rel']]; + } else { + $rel = $d['rel']; + } + + if (isset($this->_deps_type_trans[$d['type']])) { + $type = ucfirst($this->_deps_type_trans[$d['type']]); + } else { + $type = $d['type']; + } + + if (isset($d['name'])) { + $name = $d['name']; + } else { + $name = ''; + } + + if (isset($d['version'])) { + $version = $d['version']; + } else { + $version = ''; + } + + $data['data'][] = array($req, $type, $name, $rel, $version); + } + } else { // package.xml 2.0 dependencies display + require_once 'PEAR/Dependency2.php'; + $deps = $info->getDependencies(); + $reg = &$this->config->getRegistry(); + if (is_array($deps)) { + $d = new PEAR_Dependency2($this->config, array(), ''); + $data = array( + 'caption' => 'Dependencies for ' . $info->getPackage(), + 'border' => true, + 'headline' => array("Required?", "Type", "Name", 'Versioning', 'Group'), + ); + foreach ($deps as $type => $subd) { + $req = ($type == 'required') ? 'Yes' : 'No'; + if ($type == 'group') { + $group = $subd['attribs']['name']; + } else { + $group = ''; + } + if (!isset($subd[0])) { + $subd = array($subd); + } + foreach ($subd as $groupa) { + foreach ($groupa as $deptype => $depinfo) { + if ($deptype == 'attribs') { + continue; + } + if ($deptype == 'pearinstaller') { + $deptype = 'pear Installer'; + } + if (!isset($depinfo[0])) { + $depinfo = array($depinfo); + } + foreach ($depinfo as $inf) { + $name = ''; + if (isset($inf['channel'])) { + $alias = $reg->channelAlias($inf['channel']); + if (!$alias) { + $alias = '(channel?) ' .$inf['channel']; + } + $name = $alias . '/'; + } + if (isset($inf['name'])) { + $name .= $inf['name']; + } elseif (isset($inf['pattern'])) { + $name .= $inf['pattern']; + } else { + $name .= ''; + } + if (isset($inf['uri'])) { + $name .= ' [' . $inf['uri'] . ']'; + } + if (isset($inf['conflicts'])) { + $ver = 'conflicts'; + } else { + $ver = $d->_getExtraString($inf); + } + $data['data'][] = array($req, ucfirst($deptype), $name, + $ver, $group); + } + } + } + } + } + } + + $this->ui->outputData($data, $command); + return true; + } + + // Fallback + $this->ui->outputData("This package does not have any dependencies.", $command); + } + + // }}} + // {{{ doSign() + + function doSign($command, $options, $params) + { + require_once 'System.php'; + require_once 'Archive/Tar.php'; + // should move most of this code into PEAR_Packager + // so it'll be easy to implement "pear package --sign" + if (sizeof($params) != 1) { + return $this->raiseError("bad parameter(s), try \"help $command\""); + } + if (!file_exists($params[0])) { + return $this->raiseError("file does not exist: $params[0]"); + } + $obj = $this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromTgzFile($params[0], PEAR_VALIDATE_NORMAL); + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + $tar = new Archive_Tar($params[0]); + $tmpdir = System::mktemp('-d pearsign'); + if (!$tar->extractList('package2.xml package.xml package.sig', $tmpdir)) { + return $this->raiseError("failed to extract tar file"); + } + if (file_exists("$tmpdir/package.sig")) { + return $this->raiseError("package already signed"); + } + $packagexml = 'package.xml'; + if (file_exists("$tmpdir/package2.xml")) { + $packagexml = 'package2.xml'; + } + if (file_exists("$tmpdir/package.sig")) { + unlink("$tmpdir/package.sig"); + } + if (!file_exists("$tmpdir/$packagexml")) { + return $this->raiseError("Extracted file $tmpdir/$packagexml not found."); + } + $input = $this->ui->userDialog($command, + array('GnuPG Passphrase'), + array('password')); + if (!isset($input[0])) { + //use empty passphrase + $input[0] = ''; + } + + $devnull = (isset($options['verbose'])) ? '' : ' 2>/dev/null'; + $gpg = popen("gpg --batch --passphrase-fd 0 --armor --detach-sign --output $tmpdir/package.sig $tmpdir/$packagexml" . $devnull, "w"); + if (!$gpg) { + return $this->raiseError("gpg command failed"); + } + fwrite($gpg, "$input[0]\n"); + if (pclose($gpg) || !file_exists("$tmpdir/package.sig")) { + return $this->raiseError("gpg sign failed"); + } + if (!$tar->addModify("$tmpdir/package.sig", '', $tmpdir)) { + return $this->raiseError('failed adding signature to file'); + } + + $this->ui->outputData("Package signed.", $command); + return true; + } + + // }}} + + /** + * For unit testing purposes + */ + function &getInstaller(&$ui) + { + if (!class_exists('PEAR_Installer')) { + require_once 'PEAR/Installer.php'; + } + $a = &new PEAR_Installer($ui); + return $a; + } + + /** + * For unit testing purposes + */ + function &getCommandPackaging(&$ui, &$config) + { + if (!class_exists('PEAR_Command_Packaging')) { + if ($fp = @fopen('PEAR/Command/Packaging.php', 'r', true)) { + fclose($fp); + include_once 'PEAR/Command/Packaging.php'; + } + } + + if (class_exists('PEAR_Command_Packaging')) { + $a = &new PEAR_Command_Packaging($ui, $config); + } else { + $a = null; + } + return $a; + } + + // {{{ doMakeRPM() + + function doMakeRPM($command, $options, $params) + { + + // Check to see if PEAR_Command_Packaging is installed, and + // transparently switch to use the "make-rpm-spec" command from it + // instead, if it does. Otherwise, continue to use the old version + // of "makerpm" supplied with this package (PEAR). + $packaging_cmd = $this->getCommandPackaging($this->ui, $this->config); + if ($packaging_cmd !== null) { + $this->ui->outputData('PEAR_Command_Packaging is installed; using '. + 'newer "make-rpm-spec" command instead'); + return $packaging_cmd->run('make-rpm-spec', $options, $params); + } else { + $this->ui->outputData('WARNING: "pear makerpm" is no longer available; an '. + 'improved version is available via "pear make-rpm-spec", which '. + 'is available by installing PEAR_Command_Packaging'); + } + return true; + } + + function doConvert($command, $options, $params) + { + $packagexml = isset($params[0]) ? $params[0] : 'package.xml'; + $newpackagexml = isset($params[1]) ? $params[1] : dirname($packagexml) . + DIRECTORY_SEPARATOR . 'package2.xml'; + $pkg = &$this->getPackageFile($this->config, $this->_debug); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pf = $pkg->fromPackageFile($packagexml, PEAR_VALIDATE_NORMAL); + PEAR::staticPopErrorHandling(); + if (!PEAR::isError($pf)) { + if (is_a($pf, 'PEAR_PackageFile_v2')) { + $this->ui->outputData($packagexml . ' is already a package.xml version 2.0'); + return true; + } + $gen = &$pf->getDefaultGenerator(); + $newpf = &$gen->toV2(); + $newpf->setPackagefile($newpackagexml); + $gen = &$newpf->getDefaultGenerator(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $state = (isset($options['flat']) ? PEAR_VALIDATE_PACKAGING : PEAR_VALIDATE_NORMAL); + $saved = $gen->toPackageFile(dirname($newpackagexml), $state, + basename($newpackagexml)); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($saved)) { + if (is_array($saved->getUserInfo())) { + foreach ($saved->getUserInfo() as $warning) { + $this->ui->outputData($warning['message']); + } + } + $this->ui->outputData($saved->getMessage()); + return true; + } + $this->ui->outputData('Wrote new version 2.0 package.xml to "' . $saved . '"'); + return true; + } else { + if (is_array($pf->getUserInfo())) { + foreach ($pf->getUserInfo() as $warning) { + $this->ui->outputData($warning['message']); + } + } + return $this->raiseError($pf); + } + } + + // }}} +} + +?> diff --git a/PEAR/Command/Package.xml b/PEAR/Command/Package.xml new file mode 100644 index 0000000..e3f6a55 --- /dev/null +++ b/PEAR/Command/Package.xml @@ -0,0 +1,194 @@ + + + Build Package + doPackage + p + + + Z + Do not gzip the package file + + + n + Print the name of the packaged file. + + + [descfile] [descfile2] +Creates a PEAR package from its description file (usually called +package.xml). If a second packagefile is passed in, then +the packager will check to make sure that one is a package.xml +version 1.0, and the other is a package.xml version 2.0. The +package.xml version 1.0 will be saved as "package.xml" in the archive, +and the other as "package2.xml" in the archive" + + + + Validate Package Consistency + doPackageValidate + pv + + + + + + Run a "cvs diff" for all files in a package + doCvsDiff + cd + + + q + Be quiet + + + Q + Be really quiet + + + D + Diff against revision of DATE + DATE + + + R + Diff against tag for package release REL + REL + + + r + Diff against revision REV + REV + + + c + Generate context diff + + + u + Generate unified diff + + + i + Ignore case, consider upper- and lower-case letters equivalent + + + b + Ignore changes in amount of white space + + + B + Ignore changes that insert or delete blank lines + + + Report only whether the files differ, no details + + + n + Don't do anything, just pretend + + + <package.xml> +Compares all the files in a package. Without any options, this +command will compare the current code with the last checked-in code. +Using the -r or -R option you may compare the current code with that +of a specific release. + + + + Set CVS Release Tag + doCvsTag + ct + + + q + Be quiet + + + Q + Be really quiet + + + F + Move (slide) tag if it exists + + + d + Remove tag + + + n + Don't do anything, just pretend + + + <package.xml> +Sets a CVS tag on all files in a package. Use this command after you have +packaged a distribution tarball with the "package" command to tag what +revisions of what files were in that release. If need to fix something +after running cvstag once, but before the tarball is released to the public, +use the "slide" option to move the release tag. + + + + Show package dependencies + doPackageDependencies + pd + + +List all dependencies the package has. + + + Sign a package distribution file + doSign + si + + <package-file> +Signs a package distribution (.tar or .tgz) file with GnuPG. + + + Builds an RPM spec file from a PEAR package + doMakeRPM + rpm + + + t + FILE + Use FILE as RPM spec file template + + + p + FORMAT + Use FORMAT as format string for RPM package name, %s is replaced +by the PEAR package name, defaults to "PEAR::%s". + + + <package-file> + +Creates an RPM .spec file for wrapping a PEAR package inside an RPM +package. Intended to be used from the SPECS directory, with the PEAR +package tarball in the SOURCES directory: + +$ pear makerpm ../SOURCES/Net_Socket-1.0.tgz +Wrote RPM spec file PEAR::Net_Geo-1.0.spec +$ rpm -bb PEAR::Net_Socket-1.0.spec +... +Wrote: /usr/src/redhat/RPMS/i386/PEAR::Net_Socket-1.0-1.i386.rpm + + + + Convert a package.xml 1.0 to package.xml 2.0 format + doConvert + c2 + + + f + do not beautify the filelist. + + + [descfile] [descfile2] +Converts a package.xml in 1.0 format into a package.xml +in 2.0 format. The new file will be named package2.xml by default, +and package.xml will be used as the old file by default. +This is not the most intelligent conversion, and should only be +used for automated conversion or learning the format. + + + diff --git a/PEAR/Command/Pickle.php b/PEAR/Command/Pickle.php new file mode 100644 index 0000000..6137aa8 --- /dev/null +++ b/PEAR/Command/Pickle.php @@ -0,0 +1,376 @@ + + * @copyright 2005-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Pickle.php,v 1.6 2006/05/12 02:38:58 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for login/logout + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 2005-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.1 + */ + +class PEAR_Command_Pickle extends PEAR_Command_Common +{ + var $commands = array( + 'pickle' => array( + 'summary' => 'Build PECL Package', + 'function' => 'doPackage', + 'shortcut' => 'pi', + 'options' => array( + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'Do not gzip the package file' + ), + 'showname' => array( + 'shortopt' => 'n', + 'doc' => 'Print the name of the packaged file.', + ), + ), + 'doc' => '[descfile] +Creates a PECL package from its package2.xml file. + +An automatic conversion will be made to a package.xml 1.0 and written out to +disk in the current directory as "package.xml". Note that +only simple package.xml 2.0 will be converted. package.xml 2.0 with: + + - dependency types other than required/optional PECL package/ext/php/pearinstaller + - more than one extsrcrelease or zendextsrcrelease + - zendextbinrelease, extbinrelease, phprelease, or bundle release type + - dependency groups + - ignore tags in release filelist + - tasks other than replace + - custom roles + +will cause pickle to fail, and output an error message. If your package2.xml +uses any of these features, you are best off using PEAR_PackageFileManager to +generate both package.xml. +' + ), + ); + + /** + * PEAR_Command_Package constructor. + * + * @access public + */ + function PEAR_Command_Pickle(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + + /** + * For unit-testing ease + * + * @return PEAR_Packager + */ + function &getPackager() + { + if (!class_exists('PEAR_Packager')) { + require_once 'PEAR/Packager.php'; + } + $a = &new PEAR_Packager; + return $a; + } + + /** + * For unit-testing ease + * + * @param PEAR_Config $config + * @param bool $debug + * @param string|null $tmpdir + * @return PEAR_PackageFile + */ + function &getPackageFile($config, $debug = false, $tmpdir = null) + { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (!class_exists('PEAR/PackageFile.php')) { + require_once 'PEAR/PackageFile.php'; + } + $a = &new PEAR_PackageFile($config, $debug, $tmpdir); + $common = new PEAR_Common; + $common->ui = $this->ui; + $a->setLogger($common); + return $a; + } + + function doPackage($command, $options, $params) + { + $this->output = ''; + $pkginfofile = isset($params[0]) ? $params[0] : 'package2.xml'; + $packager = &$this->getPackager(); + if (PEAR::isError($err = $this->_convertPackage($pkginfofile))) { + return $err; + } + $compress = empty($options['nocompress']) ? true : false; + $result = $packager->package($pkginfofile, $compress, 'package.xml'); + if (PEAR::isError($result)) { + return $this->raiseError($result); + } + // Don't want output, only the package file name just created + if (isset($options['showname'])) { + $this->ui->outputData($result, $command); + } + return true; + } + + function _convertPackage($packagexml) + { + $pkg = &$this->getPackageFile($this->config); + $pf2 = &$pkg->fromPackageFile($packagexml, PEAR_VALIDATE_NORMAL); + if (!is_a($pf2, 'PEAR_PackageFile_v2')) { + return $this->raiseError('Cannot process "' . + $packagexml . '", is not a package.xml 2.0'); + } + require_once 'PEAR/PackageFile/v1.php'; + $pf = new PEAR_PackageFile_v1; + $pf->setConfig($this->config); + if ($pf2->getPackageType() != 'extsrc' && $pf2->getPackageType() != 'zendextsrc') { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", is not an extension source package. Using a PEAR_PackageFileManager-based ' . + 'script is an option'); + } + if (is_array($pf2->getUsesRole())) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains custom roles. Using a PEAR_PackageFileManager-based script or ' . + 'the convert command is an option'); + } + if (is_array($pf2->getUsesTask())) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains custom tasks. Using a PEAR_PackageFileManager-based script or ' . + 'the convert command is an option'); + } + $deps = $pf2->getDependencies(); + if (isset($deps['group'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains dependency groups. Using a PEAR_PackageFileManager-based script ' . + 'or the convert command is an option'); + } + if (isset($deps['required']['subpackage']) || + isset($deps['optional']['subpackage'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains subpackage dependencies. Using a PEAR_PackageFileManager-based '. + 'script is an option'); + } + if (isset($deps['required']['os'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains os dependencies. Using a PEAR_PackageFileManager-based '. + 'script is an option'); + } + if (isset($deps['required']['arch'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains arch dependencies. Using a PEAR_PackageFileManager-based '. + 'script is an option'); + } + $pf->setPackage($pf2->getPackage()); + $pf->setSummary($pf2->getSummary()); + $pf->setDescription($pf2->getDescription()); + foreach ($pf2->getMaintainers() as $maintainer) { + $pf->addMaintainer($maintainer['role'], $maintainer['handle'], + $maintainer['name'], $maintainer['email']); + } + $pf->setVersion($pf2->getVersion()); + $pf->setDate($pf2->getDate()); + $pf->setLicense($pf2->getLicense()); + $pf->setState($pf2->getState()); + $pf->setNotes($pf2->getNotes()); + $pf->addPhpDep($deps['required']['php']['min'], 'ge'); + if (isset($deps['required']['php']['max'])) { + $pf->addPhpDep($deps['required']['php']['max'], 'le'); + } + if (isset($deps['required']['package'])) { + if (!isset($deps['required']['package'][0])) { + $deps['required']['package'] = array($deps['required']['package']); + } + foreach ($deps['required']['package'] as $dep) { + if (!isset($dep['channel'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains uri-based dependency on a package. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if ($dep['channel'] != 'pear.php.net' && $dep['channel'] != 'pecl.php.net') { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains dependency on a non-standard channel package. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if (isset($dep['conflicts'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains conflicts dependency. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if (isset($dep['exclude'])) { + $this->ui->outputData('WARNING: exclude tags are ignored in conversion'); + } + if (isset($dep['min'])) { + $pf->addPackageDep($dep['name'], $dep['min'], 'ge'); + } + if (isset($dep['max'])) { + $pf->addPackageDep($dep['name'], $dep['max'], 'le'); + } + } + } + if (isset($deps['required']['extension'])) { + if (!isset($deps['required']['extension'][0])) { + $deps['required']['extension'] = array($deps['required']['extension']); + } + foreach ($deps['required']['extension'] as $dep) { + if (isset($dep['conflicts'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains conflicts dependency. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if (isset($dep['exclude'])) { + $this->ui->outputData('WARNING: exclude tags are ignored in conversion'); + } + if (isset($dep['min'])) { + $pf->addExtensionDep($dep['name'], $dep['min'], 'ge'); + } + if (isset($dep['max'])) { + $pf->addExtensionDep($dep['name'], $dep['max'], 'le'); + } + } + } + if (isset($deps['optional']['package'])) { + if (!isset($deps['optional']['package'][0])) { + $deps['optional']['package'] = array($deps['optional']['package']); + } + foreach ($deps['optional']['package'] as $dep) { + if (!isset($dep['channel'])) { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains uri-based dependency on a package. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if ($dep['channel'] != 'pear.php.net' && $dep['channel'] != 'pecl.php.net') { + return $this->raiseError('Cannot safely convert "' . $packagexml . '"' . + ' contains dependency on a non-standard channel package. Using a ' . + 'PEAR_PackageFileManager-based script is an option'); + } + if (isset($dep['exclude'])) { + $this->ui->outputData('WARNING: exclude tags are ignored in conversion'); + } + if (isset($dep['min'])) { + $pf->addPackageDep($dep['name'], $dep['min'], 'ge', 'yes'); + } + if (isset($dep['max'])) { + $pf->addPackageDep($dep['name'], $dep['max'], 'le', 'yes'); + } + } + } + if (isset($deps['optional']['extension'])) { + if (!isset($deps['optional']['extension'][0])) { + $deps['optional']['extension'] = array($deps['optional']['extension']); + } + foreach ($deps['optional']['extension'] as $dep) { + if (isset($dep['exclude'])) { + $this->ui->outputData('WARNING: exclude tags are ignored in conversion'); + } + if (isset($dep['min'])) { + $pf->addExtensionDep($dep['name'], $dep['min'], 'ge', 'yes'); + } + if (isset($dep['max'])) { + $pf->addExtensionDep($dep['name'], $dep['max'], 'le', 'yes'); + } + } + } + $contents = $pf2->getContents(); + $release = $pf2->getReleases(); + if (isset($releases[0])) { + return $this->raiseError('Cannot safely process "' . $packagexml . '" contains ' + . 'multiple extsrcrelease/zendextsrcrelease tags. Using a PEAR_PackageFileManager-based script ' . + 'or the convert command is an option'); + } + if ($configoptions = $pf2->getConfigureOptions()) { + foreach ($configoptions as $option) { + $pf->addConfigureOption($option['name'], $option['prompt'], + isset($option['default']) ? $option['default'] : false); + } + } + if (isset($release['filelist']['ignore'])) { + return $this->raiseError('Cannot safely process "' . $packagexml . '" contains ' + . 'ignore tags. Using a PEAR_PackageFileManager-based script or the convert' . + ' command is an option'); + } + if (isset($release['filelist']['install']) && + !isset($release['filelist']['install'][0])) { + $release['filelist']['install'] = array($release['filelist']['install']); + } + if (isset($contents['dir']['attribs']['baseinstalldir'])) { + $baseinstalldir = $contents['dir']['attribs']['baseinstalldir']; + } else { + $baseinstalldir = false; + } + if (!isset($contents['dir']['file'][0])) { + $contents['dir']['file'] = array($contents['dir']['file']); + } + foreach ($contents['dir']['file'] as $file) { + if ($baseinstalldir && !isset($file['attribs']['baseinstalldir'])) { + $file['attribs']['baseinstalldir'] = $baseinstalldir; + } + $processFile = $file; + unset($processFile['attribs']); + if (count($processFile)) { + foreach ($processFile as $name => $task) { + if ($name != $pf2->getTasksNs() . ':replace') { + return $this->raiseError('Cannot safely process "' . $packagexml . + '" contains tasks other than replace. Using a ' . + 'PEAR_PackageFileManager-based script is an option.'); + } + $file['attribs']['replace'][] = $task; + } + } + if (!in_array($file['attribs']['role'], PEAR_Common::getFileRoles())) { + return $this->raiseError('Cannot safely convert "' . $packagexml . + '", contains custom roles. Using a PEAR_PackageFileManager-based script ' . + 'or the convert command is an option'); + } + if (isset($release['filelist']['install'])) { + foreach ($release['filelist']['install'] as $installas) { + if ($installas['attribs']['name'] == $file['attribs']['name']) { + $file['attribs']['install-as'] = $installas['attribs']['as']; + } + } + } + $pf->addFile('/', $file['attribs']['name'], $file['attribs']); + } + if ($pf2->getChangeLog()) { + $this->ui->outputData('WARNING: changelog is not translated to package.xml ' . + '1.0, use PEAR_PackageFileManager-based script if you need changelog-' . + 'translation for package.xml 1.0'); + } + $gen = &$pf->getDefaultGenerator(); + $gen->toPackageFile('.'); + } +} + +?> diff --git a/PEAR/Command/Pickle.xml b/PEAR/Command/Pickle.xml new file mode 100644 index 0000000..04c85bc --- /dev/null +++ b/PEAR/Command/Pickle.xml @@ -0,0 +1,40 @@ + + + Build PECL Package + doPackage + pi + + + Z + Do not gzip the package file + + + n + Print the name of the packaged file. + + + [descfile] [descfile2] +Creates a PECL package from its description file (usually called +package.xml). If a second packagefile is passed in, then +the packager will check to make sure that one is a package.xml +version 1.0, and the other is a package.xml version 2.0. The +package.xml version 1.0 will be saved as "package.xml" in the archive, +and the other as "package2.xml" in the archive" + +If no second file is passed in, and [descfile] is a package.xml 2.0, +an automatic conversion will be made to a package.xml 1.0. Note that +only simple package.xml 2.0 will be converted. package.xml 2.0 with: + + - dependency types other than required/optional PECL package/ext/php/pearinstaller + - more than one extsrcrelease/zendextsrcrelease + - zendextbinrelease, extbinrelease, phprelease, or bundle release type + - dependency groups + - ignore tags in release filelist + - tasks other than replace + - custom roles + +will cause pickle to fail, and output an error message. If your package2.xml +uses any of these features, you are best off using PEAR_PackageFileManager to +generate both package.xml. + + \ No newline at end of file diff --git a/PEAR/Command/Registry.php b/PEAR/Command/Registry.php new file mode 100644 index 0000000..2659e4b --- /dev/null +++ b/PEAR/Command/Registry.php @@ -0,0 +1,1066 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Registry.php,v 1.79 2007/05/29 16:38:16 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for registry manipulation + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Registry extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'list' => array( + 'summary' => 'List Installed Packages In The Default Channel', + 'function' => 'doList', + 'shortcut' => 'l', + 'options' => array( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'list installed packages from this channel', + 'arg' => 'CHAN', + ), + 'allchannels' => array( + 'shortopt' => 'a', + 'doc' => 'list installed packages from all channels', + ), + 'channelinfo' => array( + 'shortopt' => 'i', + 'doc' => 'output fully channel-aware data, even on failure', + ), + ), + 'doc' => ' +If invoked without parameters, this command lists the PEAR packages +installed in your php_dir ({config php_dir}). With a parameter, it +lists the files in a package. +', + ), + 'list-files' => array( + 'summary' => 'List Files In Installed Package', + 'function' => 'doFileList', + 'shortcut' => 'fl', + 'options' => array(), + 'doc' => ' +List the files in an installed package. +' + ), + 'shell-test' => array( + 'summary' => 'Shell Script Test', + 'function' => 'doShellTest', + 'shortcut' => 'st', + 'options' => array(), + 'doc' => ' [[relation] version] +Tests if a package is installed in the system. Will exit(1) if it is not. + The version comparison operator. One of: + <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne + The version to compare with +'), + 'info' => array( + 'summary' => 'Display information about a package', + 'function' => 'doInfo', + 'shortcut' => 'in', + 'options' => array(), + 'doc' => ' +Displays information about a package. The package argument may be a +local package file, an URL to a package file, or the name of an +installed package.' + ) + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Registry constructor. + * + * @access public + */ + function PEAR_Command_Registry(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + // {{{ doList() + + function _sortinfo($a, $b) + { + $apackage = isset($a['package']) ? $a['package'] : $a['name']; + $bpackage = isset($b['package']) ? $b['package'] : $b['name']; + return strcmp($apackage, $bpackage); + } + + function doList($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $channelinfo = isset($options['channelinfo']); + if (isset($options['allchannels']) && !$channelinfo) { + return $this->doListAll($command, array(), $params); + } + if (isset($options['allchannels']) && $channelinfo) { + // allchannels with $channelinfo + unset($options['allchannels']); + $channels = $reg->getChannels(); + $errors = array(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + foreach ($channels as $channel) { + $options['channel'] = $channel->getName(); + $ret = $this->doList($command, $options, $params); + + if (PEAR::isError($ret)) { + $errors[] = $ret; + } + } + PEAR::staticPopErrorHandling(); + if (count($errors)) { + // for now, only give first error + return PEAR::raiseError($errors[0]); + } + return true; + } + + if (count($params) == 1) { + return $this->doFileList($command, $options, $params); + } + if (isset($options['channel'])) { + if ($reg->channelExists($options['channel'])) { + $channel = $reg->channelName($options['channel']); + } else { + return $this->raiseError('Channel "' . $options['channel'] .'" does not exist'); + } + } else { + $channel = $this->config->get('default_channel'); + } + $installed = $reg->packageInfo(null, null, $channel); + usort($installed, array(&$this, '_sortinfo')); + + $data = array( + 'caption' => 'Installed packages, channel ' . + $channel . ':', + 'border' => true, + 'headline' => array('Package', 'Version', 'State'), + 'channel' => $channel, + ); + if ($channelinfo) { + $data['headline'] = array('Channel', 'Package', 'Version', 'State'); + } + + foreach ($installed as $package) { + $pobj = $reg->getPackage(isset($package['package']) ? + $package['package'] : $package['name'], $channel); + if ($channelinfo) { + $packageinfo = array($pobj->getChannel(), $pobj->getPackage(), $pobj->getVersion(), + $pobj->getState() ? $pobj->getState() : null); + } else { + $packageinfo = array($pobj->getPackage(), $pobj->getVersion(), + $pobj->getState() ? $pobj->getState() : null); + } + $data['data'][] = $packageinfo; + } + if (count($installed) == 0) { + if (!$channelinfo) { + $data = '(no packages installed from channel ' . $channel . ')'; + } else { + $data = array( + 'caption' => 'Installed packages, channel ' . + $channel . ':', + 'border' => true, + 'channel' => $channel, + 'data' => '(no packages installed)', + ); + } + } + $this->ui->outputData($data, $command); + return true; + } + + function doListAll($command, $options, $params) + { + // This duplicate code is deprecated over + // list --channelinfo, which gives identical + // output for list and list --allchannels. + $reg = &$this->config->getRegistry(); + $installed = $reg->packageInfo(null, null, null); + foreach ($installed as $channel => $packages) { + usort($packages, array($this, '_sortinfo')); + $data = array( + 'caption' => 'Installed packages, channel ' . $channel . ':', + 'border' => true, + 'headline' => array('Package', 'Version', 'State'), + 'channel' => $channel + ); + foreach ($packages as $package) { + $pobj = $reg->getPackage(isset($package['package']) ? + $package['package'] : $package['name'], $channel); + $data['data'][] = array($pobj->getPackage(), $pobj->getVersion(), + $pobj->getState() ? $pobj->getState() : null); + } + if (count($packages)==0) { + $data = array( + 'caption' => 'Installed packages, channel ' . $channel . ':', + 'border' => true, + 'data' => array(array('(no packages installed)')), + 'channel' => $channel + ); + } + $this->ui->outputData($data, $command); + } + return true; + } + + function doFileList($command, $options, $params) + { + if (count($params) != 1) { + return $this->raiseError('list-files expects 1 parameter'); + } + $reg = &$this->config->getRegistry(); + $fp = false; + if (!is_dir($params[0]) && (file_exists($params[0]) || $fp = @fopen($params[0], + 'r'))) { + if ($fp) { + fclose($fp); + } + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + $pkg = &new PEAR_PackageFile($this->config, $this->_debug); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $info = &$pkg->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + PEAR::staticPopErrorHandling(); + $headings = array('Package File', 'Install Path'); + $installed = false; + } else { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $parsed = $reg->parsePackageName($params[0], $this->config->get('default_channel')); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($parsed)) { + return $this->raiseError($parsed); + } + $info = &$reg->getPackage($parsed['package'], $parsed['channel']); + $headings = array('Type', 'Install Path'); + $installed = true; + } + if (PEAR::isError($info)) { + return $this->raiseError($info); + } + if ($info === null) { + return $this->raiseError("`$params[0]' not installed"); + } + $list = ($info->getPackagexmlVersion() == '1.0' || $installed) ? + $info->getFilelist() : $info->getContents(); + if ($installed) { + $caption = 'Installed Files For ' . $params[0]; + } else { + $caption = 'Contents of ' . basename($params[0]); + } + $data = array( + 'caption' => $caption, + 'border' => true, + 'headline' => $headings); + if ($info->getPackagexmlVersion() == '1.0' || $installed) { + foreach ($list as $file => $att) { + if ($installed) { + if (empty($att['installed_as'])) { + continue; + } + $data['data'][] = array($att['role'], $att['installed_as']); + } else { + if (isset($att['baseinstalldir']) && !in_array($att['role'], + array('test', 'data', 'doc'))) { + $dest = $att['baseinstalldir'] . DIRECTORY_SEPARATOR . + $file; + } else { + $dest = $file; + } + switch ($att['role']) { + case 'test': + case 'data': + case 'doc': + $role = $att['role']; + if ($role == 'test') { + $role .= 's'; + } + $dest = $this->config->get($role . '_dir') . DIRECTORY_SEPARATOR . + $info->getPackage() . DIRECTORY_SEPARATOR . $dest; + break; + case 'php': + default: + $dest = $this->config->get('php_dir') . DIRECTORY_SEPARATOR . + $dest; + } + $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; + $dest = preg_replace(array('!\\\\+!', '!/!', "!$ds2+!"), + array(DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR), + $dest); + $file = preg_replace('!/+!', '/', $file); + $data['data'][] = array($file, $dest); + } + } + } else { // package.xml 2.0, not installed + if (!isset($list['dir']['file'][0])) { + $list['dir']['file'] = array($list['dir']['file']); + } + foreach ($list['dir']['file'] as $att) { + $att = $att['attribs']; + $file = $att['name']; + $role = &PEAR_Installer_Role::factory($info, $att['role'], $this->config); + $role->setup($this, $info, $att, $file); + if (!$role->isInstallable()) { + $dest = '(not installable)'; + } else { + $dest = $role->processInstallation($info, $att, $file, ''); + if (PEAR::isError($dest)) { + $dest = '(Unknown role "' . $att['role'] . ')'; + } else { + list(,, $dest) = $dest; + } + } + $data['data'][] = array($file, $dest); + } + } + $this->ui->outputData($data, $command); + return true; + } + + // }}} + // {{{ doShellTest() + + function doShellTest($command, $options, $params) + { + if (count($params) < 1) { + return PEAR::raiseError('ERROR, usage: pear shell-test packagename [[relation] version]'); + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $reg = &$this->config->getRegistry(); + $info = $reg->parsePackageName($params[0], $this->config->get('default_channel')); + if (PEAR::isError($info)) { + exit(1); // invalid package name + } + $package = $info['package']; + $channel = $info['channel']; + // "pear shell-test Foo" + if (!$reg->packageExists($package, $channel)) { + if ($channel == 'pecl.php.net') { + if ($reg->packageExists($package, 'pear.php.net')) { + $channel = 'pear.php.net'; // magically change channels for extensions + } + } + } + if (sizeof($params) == 1) { + if (!$reg->packageExists($package, $channel)) { + exit(1); + } + // "pear shell-test Foo 1.0" + } elseif (sizeof($params) == 2) { + $v = $reg->packageInfo($package, 'version', $channel); + if (!$v || !version_compare("$v", "{$params[1]}", "ge")) { + exit(1); + } + // "pear shell-test Foo ge 1.0" + } elseif (sizeof($params) == 3) { + $v = $reg->packageInfo($package, 'version', $channel); + if (!$v || !version_compare("$v", "{$params[2]}", $params[1])) { + exit(1); + } + } else { + PEAR::staticPopErrorHandling(); + $this->raiseError("$command: expects 1 to 3 parameters"); + exit(1); + } + } + + // }}} + // {{{ doInfo + + function doInfo($command, $options, $params) + { + if (count($params) != 1) { + return $this->raiseError('pear info expects 1 parameter'); + } + $info = $fp = false; + $reg = &$this->config->getRegistry(); + if ((file_exists($params[0]) && is_file($params[0]) && !is_dir($params[0])) || $fp = @fopen($params[0], 'r')) { + if ($fp) { + fclose($fp); + } + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + $pkg = &new PEAR_PackageFile($this->config, $this->_debug); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $obj = &$pkg->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($obj)) { + $uinfo = $obj->getUserInfo(); + if (is_array($uinfo)) { + foreach ($uinfo as $message) { + if (is_array($message)) { + $message = $message['message']; + } + $this->ui->outputData($message); + } + } + return $this->raiseError($obj); + } + if ($obj->getPackagexmlVersion() == '1.0') { + $info = $obj->toArray(); + } else { + return $this->_doInfo2($command, $options, $params, $obj, false); + } + } else { + $parsed = $reg->parsePackageName($params[0], $this->config->get('default_channel')); + if (PEAR::isError($parsed)) { + return $this->raiseError($parsed); + } + $package = $parsed['package']; + $channel = $parsed['channel']; + $info = $reg->packageInfo($package, null, $channel); + if (isset($info['old'])) { + $obj = $reg->getPackage($package, $channel); + return $this->_doInfo2($command, $options, $params, $obj, true); + } + } + if (PEAR::isError($info)) { + return $info; + } + if (empty($info)) { + $this->raiseError("No information found for `$params[0]'"); + return; + } + unset($info['filelist']); + unset($info['dirtree']); + unset($info['changelog']); + if (isset($info['xsdversion'])) { + $info['package.xml version'] = $info['xsdversion']; + unset($info['xsdversion']); + } + if (isset($info['packagerversion'])) { + $info['packaged with PEAR version'] = $info['packagerversion']; + unset($info['packagerversion']); + } + $keys = array_keys($info); + $longtext = array('description', 'summary'); + foreach ($keys as $key) { + if (is_array($info[$key])) { + switch ($key) { + case 'maintainers': { + $i = 0; + $mstr = ''; + foreach ($info[$key] as $m) { + if ($i++ > 0) { + $mstr .= "\n"; + } + $mstr .= $m['name'] . " <"; + if (isset($m['email'])) { + $mstr .= $m['email']; + } else { + $mstr .= $m['handle'] . '@php.net'; + } + $mstr .= "> ($m[role])"; + } + $info[$key] = $mstr; + break; + } + case 'release_deps': { + $i = 0; + $dstr = ''; + foreach ($info[$key] as $d) { + if (isset($this->_deps_rel_trans[$d['rel']])) { + $rel = $this->_deps_rel_trans[$d['rel']]; + } else { + $rel = $d['rel']; + } + if (isset($this->_deps_type_trans[$d['type']])) { + $type = ucfirst($this->_deps_type_trans[$d['type']]); + } else { + $type = $d['type']; + } + if (isset($d['name'])) { + $name = $d['name'] . ' '; + } else { + $name = ''; + } + if (isset($d['version'])) { + $version = $d['version'] . ' '; + } else { + $version = ''; + } + if (isset($d['optional']) && $d['optional'] == 'yes') { + $optional = ' (optional)'; + } else { + $optional = ''; + } + $dstr .= "$type $name$rel $version$optional\n"; + } + $info[$key] = $dstr; + break; + } + case 'provides' : { + $debug = $this->config->get('verbose'); + if ($debug < 2) { + $pstr = 'Classes: '; + } else { + $pstr = ''; + } + $i = 0; + foreach ($info[$key] as $p) { + if ($debug < 2 && $p['type'] != "class") { + continue; + } + // Only print classes when verbosity mode is < 2 + if ($debug < 2) { + if ($i++ > 0) { + $pstr .= ", "; + } + $pstr .= $p['name']; + } else { + if ($i++ > 0) { + $pstr .= "\n"; + } + $pstr .= ucfirst($p['type']) . " " . $p['name']; + if (isset($p['explicit']) && $p['explicit'] == 1) { + $pstr .= " (explicit)"; + } + } + } + $info[$key] = $pstr; + break; + } + case 'configure_options' : { + foreach ($info[$key] as $i => $p) { + $info[$key][$i] = array_map(null, array_keys($p), array_values($p)); + $info[$key][$i] = array_map(create_function('$a', + 'return join(" = ",$a);'), $info[$key][$i]); + $info[$key][$i] = implode(', ', $info[$key][$i]); + } + $info[$key] = implode("\n", $info[$key]); + break; + } + default: { + $info[$key] = implode(", ", $info[$key]); + break; + } + } + } + if ($key == '_lastmodified') { + $hdate = date('Y-m-d', $info[$key]); + unset($info[$key]); + $info['Last Modified'] = $hdate; + } elseif ($key == '_lastversion') { + $info['Previous Installed Version'] = $info[$key] ? $info[$key] : '- None -'; + unset($info[$key]); + } else { + $info[$key] = trim($info[$key]); + if (in_array($key, $longtext)) { + $info[$key] = preg_replace('/ +/', ' ', $info[$key]); + } + } + } + $caption = 'About ' . $info['package'] . '-' . $info['version']; + $data = array( + 'caption' => $caption, + 'border' => true); + foreach ($info as $key => $value) { + $key = ucwords(trim(str_replace('_', ' ', $key))); + $data['data'][] = array($key, $value); + } + $data['raw'] = $info; + + $this->ui->outputData($data, 'package-info'); + } + + // }}} + + /** + * @access private + */ + function _doInfo2($command, $options, $params, &$obj, $installed) + { + $reg = &$this->config->getRegistry(); + $caption = 'About ' . $obj->getChannel() . '/' .$obj->getPackage() . '-' . + $obj->getVersion(); + $data = array( + 'caption' => $caption, + 'border' => true); + switch ($obj->getPackageType()) { + case 'php' : + $release = 'PEAR-style PHP-based Package'; + break; + case 'extsrc' : + $release = 'PECL-style PHP extension (source code)'; + break; + case 'zendextsrc' : + $release = 'PECL-style Zend extension (source code)'; + break; + case 'extbin' : + $release = 'PECL-style PHP extension (binary)'; + break; + case 'zendextbin' : + $release = 'PECL-style Zend extension (binary)'; + break; + case 'bundle' : + $release = 'Package bundle (collection of packages)'; + break; + } + $extends = $obj->getExtends(); + $extends = $extends ? + $obj->getPackage() . ' (extends ' . $extends . ')' : $obj->getPackage(); + if ($src = $obj->getSourcePackage()) { + $extends .= ' (source package ' . $src['channel'] . '/' . $src['package'] . ')'; + } + $info = array( + 'Release Type' => $release, + 'Name' => $extends, + 'Channel' => $obj->getChannel(), + 'Summary' => preg_replace('/ +/', ' ', $obj->getSummary()), + 'Description' => preg_replace('/ +/', ' ', $obj->getDescription()), + ); + $info['Maintainers'] = ''; + foreach (array('lead', 'developer', 'contributor', 'helper') as $role) { + $leads = $obj->{"get{$role}s"}(); + if (!$leads) { + continue; + } + if (isset($leads['active'])) { + $leads = array($leads); + } + foreach ($leads as $lead) { + if (!empty($info['Maintainers'])) { + $info['Maintainers'] .= "\n"; + } + $info['Maintainers'] .= $lead['name'] . ' <'; + $info['Maintainers'] .= $lead['email'] . "> ($role)"; + } + } + $info['Release Date'] = $obj->getDate(); + if ($time = $obj->getTime()) { + $info['Release Date'] .= ' ' . $time; + } + $info['Release Version'] = $obj->getVersion() . ' (' . $obj->getState() . ')'; + $info['API Version'] = $obj->getVersion('api') . ' (' . $obj->getState('api') . ')'; + $info['License'] = $obj->getLicense(); + $uri = $obj->getLicenseLocation(); + if ($uri) { + if (isset($uri['uri'])) { + $info['License'] .= ' (' . $uri['uri'] . ')'; + } else { + $extra = $obj->getInstalledLocation($info['filesource']); + if ($extra) { + $info['License'] .= ' (' . $uri['filesource'] . ')'; + } + } + } + $info['Release Notes'] = $obj->getNotes(); + if ($compat = $obj->getCompatible()) { + if (!isset($compat[0])) { + $compat = array($compat); + } + $info['Compatible with'] = ''; + foreach ($compat as $package) { + $info['Compatible with'] .= $package['channel'] . '/' . $package['name'] . + "\nVersions >= " . $package['min'] . ', <= ' . $package['max']; + if (isset($package['exclude'])) { + if (is_array($package['exclude'])) { + $package['exclude'] = implode(', ', $package['exclude']); + } + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + $info['Not Compatible with'] .= $package['channel'] . '/' . + $package['name'] . "\nVersions " . $package['exclude']; + } + } + } + $usesrole = $obj->getUsesrole(); + if ($usesrole) { + if (!isset($usesrole[0])) { + $usesrole = array($usesrole); + } + foreach ($usesrole as $roledata) { + if (isset($info['Uses Custom Roles'])) { + $info['Uses Custom Roles'] .= "\n"; + } else { + $info['Uses Custom Roles'] = ''; + } + if (isset($roledata['package'])) { + $rolepackage = $reg->parsedPackageNameToString($roledata, true); + } else { + $rolepackage = $roledata['uri']; + } + $info['Uses Custom Roles'] .= $roledata['role'] . ' (' . $rolepackage . ')'; + } + } + $usestask = $obj->getUsestask(); + if ($usestask) { + if (!isset($usestask[0])) { + $usestask = array($usestask); + } + foreach ($usestask as $taskdata) { + if (isset($info['Uses Custom Tasks'])) { + $info['Uses Custom Tasks'] .= "\n"; + } else { + $info['Uses Custom Tasks'] = ''; + } + if (isset($taskdata['package'])) { + $taskpackage = $reg->parsedPackageNameToString($taskdata, true); + } else { + $taskpackage = $taskdata['uri']; + } + $info['Uses Custom Tasks'] .= $taskdata['task'] . ' (' . $taskpackage . ')'; + } + } + $deps = $obj->getDependencies(); + $info['Required Dependencies'] = 'PHP version ' . $deps['required']['php']['min']; + if (isset($deps['required']['php']['max'])) { + $info['Required Dependencies'] .= '-' . $deps['required']['php']['max'] . "\n"; + } else { + $info['Required Dependencies'] .= "\n"; + } + if (isset($deps['required']['php']['exclude'])) { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + if (is_array($deps['required']['php']['exclude'])) { + $deps['required']['php']['exclude'] = + implode(', ', $deps['required']['php']['exclude']); + } + $info['Not Compatible with'] .= "PHP versions\n " . + $deps['required']['php']['exclude']; + } + $info['Required Dependencies'] .= 'PEAR installer version'; + if (isset($deps['required']['pearinstaller']['max'])) { + $info['Required Dependencies'] .= 's ' . + $deps['required']['pearinstaller']['min'] . '-' . + $deps['required']['pearinstaller']['max']; + } else { + $info['Required Dependencies'] .= ' ' . + $deps['required']['pearinstaller']['min'] . ' or newer'; + } + if (isset($deps['required']['pearinstaller']['exclude'])) { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + if (is_array($deps['required']['pearinstaller']['exclude'])) { + $deps['required']['pearinstaller']['exclude'] = + implode(', ', $deps['required']['pearinstaller']['exclude']); + } + $info['Not Compatible with'] .= "PEAR installer\n Versions " . + $deps['required']['pearinstaller']['exclude']; + } + foreach (array('Package', 'Extension') as $type) { + $index = strtolower($type); + if (isset($deps['required'][$index])) { + if (isset($deps['required'][$index]['name'])) { + $deps['required'][$index] = array($deps['required'][$index]); + } + foreach ($deps['required'][$index] as $package) { + if (isset($package['conflicts'])) { + $infoindex = 'Not Compatible with'; + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + } else { + $infoindex = 'Required Dependencies'; + $info[$infoindex] .= "\n"; + } + if ($index == 'extension') { + $name = $package['name']; + } else { + if (isset($package['channel'])) { + $name = $package['channel'] . '/' . $package['name']; + } else { + $name = '__uri/' . $package['name'] . ' (static URI)'; + } + } + $info[$infoindex] .= "$type $name"; + if (isset($package['uri'])) { + $info[$infoindex] .= "\n Download URI: $package[uri]"; + continue; + } + if (isset($package['max']) && isset($package['min'])) { + $info[$infoindex] .= " \n Versions " . + $package['min'] . '-' . $package['max']; + } elseif (isset($package['min'])) { + $info[$infoindex] .= " \n Version " . + $package['min'] . ' or newer'; + } elseif (isset($package['max'])) { + $info[$infoindex] .= " \n Version " . + $package['max'] . ' or older'; + } + if (isset($package['recommended'])) { + $info[$infoindex] .= "\n Recommended version: $package[recommended]"; + } + if (isset($package['exclude'])) { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + if (is_array($package['exclude'])) { + $package['exclude'] = implode(', ', $package['exclude']); + } + $package['package'] = $package['name']; // for parsedPackageNameToString + if (isset($package['conflicts'])) { + $info['Not Compatible with'] .= '=> except '; + } + $info['Not Compatible with'] .= 'Package ' . + $reg->parsedPackageNameToString($package, true); + $info['Not Compatible with'] .= "\n Versions " . $package['exclude']; + } + } + } + } + if (isset($deps['required']['os'])) { + if (isset($deps['required']['os']['name'])) { + $dep['required']['os']['name'] = array($dep['required']['os']['name']); + } + foreach ($dep['required']['os'] as $os) { + if (isset($os['conflicts']) && $os['conflicts'] == 'yes') { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + $info['Not Compatible with'] .= "$os[name] Operating System"; + } else { + $info['Required Dependencies'] .= "\n"; + $info['Required Dependencies'] .= "$os[name] Operating System"; + } + } + } + if (isset($deps['required']['arch'])) { + if (isset($deps['required']['arch']['pattern'])) { + $dep['required']['arch']['pattern'] = array($dep['required']['os']['pattern']); + } + foreach ($dep['required']['arch'] as $os) { + if (isset($os['conflicts']) && $os['conflicts'] == 'yes') { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + $info['Not Compatible with'] .= "OS/Arch matching pattern '/$os[pattern]/'"; + } else { + $info['Required Dependencies'] .= "\n"; + $info['Required Dependencies'] .= "OS/Arch matching pattern '/$os[pattern]/'"; + } + } + } + if (isset($deps['optional'])) { + foreach (array('Package', 'Extension') as $type) { + $index = strtolower($type); + if (isset($deps['optional'][$index])) { + if (isset($deps['optional'][$index]['name'])) { + $deps['optional'][$index] = array($deps['optional'][$index]); + } + foreach ($deps['optional'][$index] as $package) { + if (isset($package['conflicts']) && $package['conflicts'] == 'yes') { + $infoindex = 'Not Compatible with'; + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + } else { + $infoindex = 'Optional Dependencies'; + if (!isset($info['Optional Dependencies'])) { + $info['Optional Dependencies'] = ''; + } else { + $info['Optional Dependencies'] .= "\n"; + } + } + if ($index == 'extension') { + $name = $package['name']; + } else { + if (isset($package['channel'])) { + $name = $package['channel'] . '/' . $package['name']; + } else { + $name = '__uri/' . $package['name'] . ' (static URI)'; + } + } + $info[$infoindex] .= "$type $name"; + if (isset($package['uri'])) { + $info[$infoindex] .= "\n Download URI: $package[uri]"; + continue; + } + if ($infoindex == 'Not Compatible with') { + // conflicts is only used to say that all versions conflict + continue; + } + if (isset($package['max']) && isset($package['min'])) { + $info[$infoindex] .= " \n Versions " . + $package['min'] . '-' . $package['max']; + } elseif (isset($package['min'])) { + $info[$infoindex] .= " \n Version " . + $package['min'] . ' or newer'; + } elseif (isset($package['max'])) { + $info[$infoindex] .= " \n Version " . + $package['min'] . ' or older'; + } + if (isset($package['recommended'])) { + $info[$infoindex] .= "\n Recommended version: $package[recommended]"; + } + if (isset($package['exclude'])) { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info['Not Compatible with'] .= "\n"; + } + if (is_array($package['exclude'])) { + $package['exclude'] = implode(', ', $package['exclude']); + } + $info['Not Compatible with'] .= "Package $package\n Versions " . + $package['exclude']; + } + } + } + } + } + if (isset($deps['group'])) { + if (!isset($deps['group'][0])) { + $deps['group'] = array($deps['group']); + } + foreach ($deps['group'] as $group) { + $info['Dependency Group ' . $group['attribs']['name']] = $group['attribs']['hint']; + $groupindex = $group['attribs']['name'] . ' Contents'; + $info[$groupindex] = ''; + foreach (array('Package', 'Extension') as $type) { + $index = strtolower($type); + if (isset($group[$index])) { + if (isset($group[$index]['name'])) { + $group[$index] = array($group[$index]); + } + foreach ($group[$index] as $package) { + if (!empty($info[$groupindex])) { + $info[$groupindex] .= "\n"; + } + if ($index == 'extension') { + $name = $package['name']; + } else { + if (isset($package['channel'])) { + $name = $package['channel'] . '/' . $package['name']; + } else { + $name = '__uri/' . $package['name'] . ' (static URI)'; + } + } + if (isset($package['uri'])) { + if (isset($package['conflicts']) && $package['conflicts'] == 'yes') { + $info[$groupindex] .= "Not Compatible with $type $name"; + } else { + $info[$groupindex] .= "$type $name"; + } + $info[$groupindex] .= "\n Download URI: $package[uri]"; + continue; + } + if (isset($package['conflicts']) && $package['conflicts'] == 'yes') { + $info[$groupindex] .= "Not Compatible with $type $name"; + continue; + } + $info[$groupindex] .= "$type $name"; + if (isset($package['max']) && isset($package['min'])) { + $info[$groupindex] .= " \n Versions " . + $package['min'] . '-' . $package['max']; + } elseif (isset($package['min'])) { + $info[$groupindex] .= " \n Version " . + $package['min'] . ' or newer'; + } elseif (isset($package['max'])) { + $info[$groupindex] .= " \n Version " . + $package['min'] . ' or older'; + } + if (isset($package['recommended'])) { + $info[$groupindex] .= "\n Recommended version: $package[recommended]"; + } + if (isset($package['exclude'])) { + if (!isset($info['Not Compatible with'])) { + $info['Not Compatible with'] = ''; + } else { + $info[$groupindex] .= "Not Compatible with\n"; + } + if (is_array($package['exclude'])) { + $package['exclude'] = implode(', ', $package['exclude']); + } + $info[$groupindex] .= " Package $package\n Versions " . + $package['exclude']; + } + } + } + } + } + } + if ($obj->getPackageType() == 'bundle') { + $info['Bundled Packages'] = ''; + foreach ($obj->getBundledPackages() as $package) { + if (!empty($info['Bundled Packages'])) { + $info['Bundled Packages'] .= "\n"; + } + if (isset($package['uri'])) { + $info['Bundled Packages'] .= '__uri/' . $package['name']; + $info['Bundled Packages'] .= "\n (URI: $package[uri]"; + } else { + $info['Bundled Packages'] .= $package['channel'] . '/' . $package['name']; + } + } + } + $info['package.xml version'] = '2.0'; + if ($installed) { + if ($obj->getLastModified()) { + $info['Last Modified'] = date('Y-m-d H:i', $obj->getLastModified()); + } + $v = $obj->getLastInstalledVersion(); + $info['Previous Installed Version'] = $v ? $v : '- None -'; + } + foreach ($info as $key => $value) { + $data['data'][] = array($key, $value); + } + $data['raw'] = $obj->getArray(); // no validation needed + + $this->ui->outputData($data, 'package-info'); + } +} + +?> diff --git a/PEAR/Command/Registry.xml b/PEAR/Command/Registry.xml new file mode 100644 index 0000000..9f4e214 --- /dev/null +++ b/PEAR/Command/Registry.xml @@ -0,0 +1,58 @@ + + + List Installed Packages In The Default Channel + doList + l + + + c + list installed packages from this channel + CHAN + + + a + list installed packages from all channels + + + i + output fully channel-aware data, even on failure + + + <package> +If invoked without parameters, this command lists the PEAR packages +installed in your php_dir ({config php_dir}). With a parameter, it +lists the files in a package. + + + + List Files In Installed Package + doFileList + fl + + <package> +List the files in an installed package. + + + + Shell Script Test + doShellTest + st + + <package> [[relation] version] +Tests if a package is installed in the system. Will exit(1) if it is not. + <relation> The version comparison operator. One of: + <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne + <version> The version to compare with + + + + Display information about a package + doInfo + in + + <package> +Displays information about a package. The package argument may be a +local package file, an URL to a package file, or the name of an +installed package. + + \ No newline at end of file diff --git a/PEAR/Command/Remote.php b/PEAR/Command/Remote.php new file mode 100644 index 0000000..c73b573 --- /dev/null +++ b/PEAR/Command/Remote.php @@ -0,0 +1,812 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Remote.php,v 1.105 2007/06/22 13:49:30 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; +require_once 'PEAR/REST.php'; + +/** + * PEAR commands for remote server querying + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Remote extends PEAR_Command_Common +{ + // {{{ command definitions + + var $commands = array( + 'remote-info' => array( + 'summary' => 'Information About Remote Packages', + 'function' => 'doRemoteInfo', + 'shortcut' => 'ri', + 'options' => array(), + 'doc' => ' +Get details on a package from the server.', + ), + 'list-upgrades' => array( + 'summary' => 'List Available Upgrades', + 'function' => 'doListUpgrades', + 'shortcut' => 'lu', + 'options' => array( + 'channelinfo' => array( + 'shortopt' => 'i', + 'doc' => 'output fully channel-aware data, even on failure', + ), + ), + 'doc' => '[preferred_state] +List releases on the server of packages you have installed where +a newer version is available with the same release state (stable etc.) +or the state passed as the second parameter.' + ), + 'remote-list' => array( + 'summary' => 'List Remote Packages', + 'function' => 'doRemoteList', + 'shortcut' => 'rl', + 'options' => array( + 'channel' => + array( + 'shortopt' => 'c', + 'doc' => 'specify a channel other than the default channel', + 'arg' => 'CHAN', + ) + ), + 'doc' => ' +Lists the packages available on the configured server along with the +latest stable release of each package.', + ), + 'search' => array( + 'summary' => 'Search remote package database', + 'function' => 'doSearch', + 'shortcut' => 'sp', + 'options' => array( + 'channel' => + array( + 'shortopt' => 'c', + 'doc' => 'specify a channel other than the default channel', + 'arg' => 'CHAN', + ), + 'allchannels' => array( + 'shortopt' => 'a', + 'doc' => 'search packages from all known channels', + ), + 'channelinfo' => array( + 'shortopt' => 'i', + 'doc' => 'output fully channel-aware data, even on failure', + ), + ), + 'doc' => '[packagename] [packageinfo] +Lists all packages which match the search parameters. The first +parameter is a fragment of a packagename. The default channel +will be used unless explicitly overridden. The second parameter +will be used to match any portion of the summary/description', + ), + 'list-all' => array( + 'summary' => 'List All Packages', + 'function' => 'doListAll', + 'shortcut' => 'la', + 'options' => array( + 'channel' => + array( + 'shortopt' => 'c', + 'doc' => 'specify a channel other than the default channel', + 'arg' => 'CHAN', + ), + 'channelinfo' => array( + 'shortopt' => 'i', + 'doc' => 'output fully channel-aware data, even on failure', + ), + ), + 'doc' => ' +Lists the packages available on the configured server along with the +latest stable release of each package.', + ), + 'download' => array( + 'summary' => 'Download Package', + 'function' => 'doDownload', + 'shortcut' => 'd', + 'options' => array( + 'nocompress' => array( + 'shortopt' => 'Z', + 'doc' => 'download an uncompressed (.tar) file', + ), + ), + 'doc' => '... +Download package tarballs. The files will be named as suggested by the +server, for example if you download the DB package and the latest stable +version of DB is 1.6.5, the downloaded file will be DB-1.6.5.tgz.', + ), + 'clear-cache' => array( + 'summary' => 'Clear Web Services Cache', + 'function' => 'doClearCache', + 'shortcut' => 'cc', + 'options' => array(), + 'doc' => ' +Clear the XML-RPC/REST cache. See also the cache_ttl configuration +parameter. +', + ), + ); + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Remote constructor. + * + * @access public + */ + function PEAR_Command_Remote(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + + function _checkChannelForStatus($channel, $chan) + { + if (PEAR::isError($chan)) { + $this->raiseError($chan); + } + if (!is_a($chan, 'PEAR_ChannelFile')) { + return $this->raiseError('Internal corruption error: invalid channel "' . + $channel . '"'); + } + $rest = new PEAR_REST($this->config); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $mirror = $this->config->get('preferred_mirror', null, + $channel); + $a = $rest->downloadHttp('http://' . $channel . + '/channel.xml', $chan->lastModified()); + PEAR::staticPopErrorHandling(); + if (!PEAR::isError($a) && $a) { + $this->ui->outputData('WARNING: channel "' . $channel . '" has ' . + 'updated its protocols, use "channel-update ' . $channel . + '" to update'); + } + } + + // {{{ doRemoteInfo() + + function doRemoteInfo($command, $options, $params) + { + if (sizeof($params) != 1) { + return $this->raiseError("$command expects one param: the remote package name"); + } + $savechannel = $channel = $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + $package = $params[0]; + $parsed = $reg->parsePackageName($package, $channel); + if (PEAR::isError($parsed)) { + return $this->raiseError('Invalid package name "' . $package . '"'); + } + + $channel = $parsed['channel']; + $this->config->set('default_channel', $channel); + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + $info = $rest->packageInfo($base, $parsed['package']); + } else { + $r = &$this->config->getRemote(); + $info = $r->call('package.info', $parsed['package']); + } + if (PEAR::isError($info)) { + $this->config->set('default_channel', $savechannel); + return $this->raiseError($info); + } + if (!isset($info['name'])) { + return $this->raiseError('No remote package "' . $package . '" was found'); + } + + $installed = $reg->packageInfo($info['name'], null, $channel); + $info['installed'] = $installed['version'] ? $installed['version'] : '- no -'; + if (is_array($info['installed'])) { + $info['installed'] = $info['installed']['release']; + } + + $this->ui->outputData($info, $command); + $this->config->set('default_channel', $savechannel); + + return true; + } + + // }}} + // {{{ doRemoteList() + + function doRemoteList($command, $options, $params) + { + $savechannel = $channel = $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + if (isset($options['channel'])) { + $channel = $options['channel']; + if ($reg->channelExists($channel)) { + $this->config->set('default_channel', $channel); + } else { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + } + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + $list_options = false; + if ($this->config->get('preferred_state') == 'stable') { + $list_options = true; + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.1', $this->config->get('preferred_mirror'))) { + // use faster list-all if available + $rest = &$this->config->getREST('1.1', array()); + $available = $rest->listAll($base, $list_options); + } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + $available = $rest->listAll($base, $list_options); + } else { + $r = &$this->config->getRemote(); + if ($channel == 'pear.php.net') { + // hack because of poor pearweb design + $available = $r->call('package.listAll', true, $list_options, false); + } else { + $available = $r->call('package.listAll', true, $list_options); + } + } + if (PEAR::isError($available)) { + $this->config->set('default_channel', $savechannel); + return $this->raiseError($available); + } + $i = $j = 0; + $data = array( + 'caption' => 'Channel ' . $channel . ' Available packages:', + 'border' => true, + 'headline' => array('Package', 'Version'), + 'channel' => $channel + ); + if (count($available)==0) { + $data = '(no packages available yet)'; + } else { + foreach ($available as $name => $info) { + $data['data'][] = array($name, (isset($info['stable']) && $info['stable']) + ? $info['stable'] : '-n/a-'); + } + } + $this->ui->outputData($data, $command); + $this->config->set('default_channel', $savechannel); + return true; + } + + // }}} + // {{{ doListAll() + + function doListAll($command, $options, $params) + { + $savechannel = $channel = $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + if (isset($options['channel'])) { + $channel = $options['channel']; + if ($reg->channelExists($channel)) { + $this->config->set('default_channel', $channel); + } else { + return $this->raiseError("Channel \"$channel\" does not exist"); + } + } + $list_options = false; + if ($this->config->get('preferred_state') == 'stable') { + $list_options = true; + } + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.1', $this->config->get('preferred_mirror'))) { + // use faster list-all if available + $rest = &$this->config->getREST('1.1', array()); + $available = $rest->listAll($base, $list_options, false); + } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + $available = $rest->listAll($base, $list_options, false); + } else { + $r = &$this->config->getRemote(); + if ($channel == 'pear.php.net') { + // hack because of poor pearweb design + $available = $r->call('package.listAll', true, $list_options, false); + } else { + $available = $r->call('package.listAll', true, $list_options); + } + } + if (PEAR::isError($available)) { + $this->config->set('default_channel', $savechannel); + return $this->raiseError('The package list could not be fetched from the remote server. Please try again. (Debug info: "' . $available->getMessage() . '")'); + } + $data = array( + 'caption' => 'All packages [Channel ' . $channel . ']:', + 'border' => true, + 'headline' => array('Package', 'Latest', 'Local'), + 'channel' => $channel, + ); + if (isset($options['channelinfo'])) { + // add full channelinfo + $data['caption'] = 'Channel ' . $channel . ' All packages:'; + $data['headline'] = array('Channel', 'Package', 'Latest', 'Local', + 'Description', 'Dependencies'); + } + $local_pkgs = $reg->listPackages($channel); + + foreach ($available as $name => $info) { + $installed = $reg->packageInfo($name, null, $channel); + if (is_array($installed['version'])) { + $installed['version'] = $installed['version']['release']; + } + $desc = $info['summary']; + if (isset($params[$name])) { + $desc .= "\n\n".$info['description']; + } + if (isset($options['mode'])) + { + if ($options['mode'] == 'installed' && !isset($installed['version'])) { + continue; + } + if ($options['mode'] == 'notinstalled' && isset($installed['version'])) { + continue; + } + if ($options['mode'] == 'upgrades' + && (!isset($installed['version']) || version_compare($installed['version'], + $info['stable'], '>='))) { + continue; + } + } + $pos = array_search(strtolower($name), $local_pkgs); + if ($pos !== false) { + unset($local_pkgs[$pos]); + } + + if (isset($info['stable']) && !$info['stable']) { + $info['stable'] = null; + } + + if (isset($options['channelinfo'])) { + // add full channelinfo + if ($info['stable'] === $info['unstable']) { + $state = $info['state']; + } else { + $state = 'stable'; + } + $latest = $info['stable'].' ('.$state.')'; + $local = ''; + if (isset($installed['version'])) { + $inst_state = $reg->packageInfo($name, 'release_state', $channel); + $local = $installed['version'].' ('.$inst_state.')'; + } + + $packageinfo = array( + $channel, + $name, + $latest, + $local, + isset($desc) ? $desc : null, + isset($info['deps']) ? $info['deps'] : null, + ); + } else { + $packageinfo = array( + $reg->channelAlias($channel) . '/' . $name, + isset($info['stable']) ? $info['stable'] : null, + isset($installed['version']) ? $installed['version'] : null, + isset($desc) ? $desc : null, + isset($info['deps']) ? $info['deps'] : null, + ); + } + $data['data'][$info['category']][] = $packageinfo; + } + + if (isset($options['mode']) && in_array($options['mode'], array('notinstalled', 'upgrades'))) { + $this->config->set('default_channel', $savechannel); + $this->ui->outputData($data, $command); + return true; + } + foreach ($local_pkgs as $name) { + $info = &$reg->getPackage($name, $channel); + $data['data']['Local'][] = array( + $reg->channelAlias($channel) . '/' . $info->getPackage(), + '', + $info->getVersion(), + $info->getSummary(), + $info->getDeps() + ); + } + + $this->config->set('default_channel', $savechannel); + $this->ui->outputData($data, $command); + return true; + } + + // }}} + // {{{ doSearch() + + function doSearch($command, $options, $params) + { + if ((!isset($params[0]) || empty($params[0])) + && (!isset($params[1]) || empty($params[1]))) + { + return $this->raiseError('no valid search string supplied'); + }; + + $channelinfo = isset($options['channelinfo']); + $reg = &$this->config->getRegistry(); + if (isset($options['allchannels'])) { + // search all channels + unset($options['allchannels']); + $channels = $reg->getChannels(); + $errors = array(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + foreach ($channels as $channel) { + if ($channel->getName() != '__uri') { + $options['channel'] = $channel->getName(); + $ret = $this->doSearch($command, $options, $params); + if (PEAR::isError($ret)) { + $errors[] = $ret; + } + } + } + PEAR::staticPopErrorHandling(); + if (count($errors) !== 0) { + // for now, only give first error + return PEAR::raiseError($errors[0]); + } + + return true; + } + + $savechannel = $channel = $this->config->get('default_channel'); + $package = $params[0]; + $summary = isset($params[1]) ? $params[1] : false; + if (isset($options['channel'])) { + $reg = &$this->config->getRegistry(); + $channel = $options['channel']; + if ($reg->channelExists($channel)) { + $this->config->set('default_channel', $channel); + } else { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + } + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + $available = $rest->listAll($base, false, false, $package, $summary); + } else { + $r = &$this->config->getRemote(); + $available = $r->call('package.search', $package, $summary, true, + $this->config->get('preferred_state') == 'stable', true); + } + if (PEAR::isError($available)) { + $this->config->set('default_channel', $savechannel); + return $this->raiseError($available); + } + if (!$available && !$channelinfo) { + // clean exit when not found, no error ! + $data = 'no packages found that match pattern "' . $package . '", for channel '.$channel.'.'; + $this->ui->outputData($data); + $this->config->set('default_channel', $channel); + return true; + } + if ($channelinfo) { + $data = array( + 'caption' => 'Matched packages, channel ' . $channel . ':', + 'border' => true, + 'headline' => array('Channel', 'Package', 'Stable/(Latest)', 'Local'), + 'channel' => $channel + ); + } else { + $data = array( + 'caption' => 'Matched packages, channel ' . $channel . ':', + 'border' => true, + 'headline' => array('Package', 'Stable/(Latest)', 'Local'), + 'channel' => $channel + ); + } + + if (!$available && $channelinfo) { + unset($data['headline']); + $data['data'] = 'No packages found that match pattern "' . $package . '".'; + $available = array(); + } + foreach ($available as $name => $info) { + $installed = $reg->packageInfo($name, null, $channel); + $desc = $info['summary']; + if (isset($params[$name])) + $desc .= "\n\n".$info['description']; + + if (!isset($info['stable']) || !$info['stable']) { + $version_remote = 'none'; + } else { + if ($info['unstable']) { + $version_remote = $info['unstable']; + } else { + $version_remote = $info['stable']; + } + $version_remote .= ' ('.$info['state'].')'; + } + $version = is_array($installed['version']) ? $installed['version']['release'] : + $installed['version']; + if ($channelinfo) { + $packageinfo = array( + $channel, + $name, + $version_remote, + $version, + $desc, + ); + } else { + $packageinfo = array( + $name, + $version_remote, + $version, + $desc, + ); + } + $data['data'][$info['category']][] = $packageinfo; + } + $this->ui->outputData($data, $command); + $this->config->set('default_channel', $channel); + return true; + } + + // }}} + function &getDownloader($options) + { + if (!class_exists('PEAR_Downloader')) { + require_once 'PEAR/Downloader.php'; + } + $a = &new PEAR_Downloader($this->ui, $options, $this->config); + return $a; + } + // {{{ doDownload() + + function doDownload($command, $options, $params) + { + // make certain that dependencies are ignored + $options['downloadonly'] = 1; + + // eliminate error messages for preferred_state-related errors + /* TODO: Should be an option, but until now download does respect + prefered state */ + /* $options['ignorepreferred_state'] = 1; */ + // eliminate error messages for preferred_state-related errors + + $downloader = &$this->getDownloader($options); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $e = $downloader->setDownloadDir(getcwd()); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($e)) { + return $this->raiseError('Current directory is not writeable, cannot download'); + } + $errors = array(); + $downloaded = array(); + $err = $downloader->download($params); + if (PEAR::isError($err)) { + return $err; + } + $errors = $downloader->getErrorMsgs(); + if (count($errors)) { + foreach ($errors as $error) { + $this->ui->outputData($error); + } + return $this->raiseError("$command failed"); + } + $downloaded = $downloader->getDownloadedPackages(); + foreach ($downloaded as $pkg) { + $this->ui->outputData("File $pkg[file] downloaded", $command); + } + return true; + } + + function downloadCallback($msg, $params = null) + { + if ($msg == 'done') { + $this->bytes_downloaded = $params; + } + } + + // }}} + // {{{ doListUpgrades() + + function doListUpgrades($command, $options, $params) + { + require_once 'PEAR/Common.php'; + if (isset($params[0]) && !is_array(PEAR_Common::betterStates($params[0]))) { + return $this->raiseError($params[0] . ' is not a valid state (stable/beta/alpha/devel/etc.) try "pear help list-upgrades"'); + } + $savechannel = $channel = $this->config->get('default_channel'); + $reg = &$this->config->getRegistry(); + foreach ($reg->listChannels() as $channel) { + $inst = array_flip($reg->listPackages($channel)); + if (!count($inst)) { + continue; + } + if ($channel == '__uri') { + continue; + } + $this->config->set('default_channel', $channel); + if (empty($params[0])) { + $state = $this->config->get('preferred_state'); + } else { + $state = $params[0]; + } + $caption = $channel . ' Available Upgrades'; + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', array()); + if (empty($state) || $state == 'any') { + $state = false; + } else { + $caption .= ' (' . implode(', ', PEAR_Common::betterStates($state, true)) . ')'; + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $latest = $rest->listLatestUpgrades($base, $state, $inst, $channel, $reg); + PEAR::staticPopErrorHandling(); + } else { + $remote = &$this->config->getRemote(); + $remote->pushErrorHandling(PEAR_ERROR_RETURN); + if (empty($state) || $state == 'any') { + $latest = $remote->call("package.listLatestReleases"); + } else { + $latest = $remote->call("package.listLatestReleases", $state); + $caption .= ' (' . implode(', ', PEAR_Common::betterStates($state, true)) . ')'; + } + $remote->popErrorHandling(); + } + if (PEAR::isError($latest)) { + $this->ui->outputData($latest->getMessage()); + continue; + } + $caption .= ':'; + if (PEAR::isError($latest)) { + $this->config->set('default_channel', $savechannel); + return $latest; + } + $data = array( + 'caption' => $caption, + 'border' => 1, + 'headline' => array('Channel', 'Package', 'Local', 'Remote', 'Size'), + 'channel' => $channel + ); + foreach ((array)$latest as $pkg => $info) { + $package = strtolower($pkg); + if (!isset($inst[$package])) { + // skip packages we don't have installed + continue; + } + extract($info); + $inst_version = $reg->packageInfo($package, 'version', $channel); + $inst_state = $reg->packageInfo($package, 'release_state', $channel); + if (version_compare("$version", "$inst_version", "le")) { + // installed version is up-to-date + continue; + } + if ($filesize >= 20480) { + $filesize += 1024 - ($filesize % 1024); + $fs = sprintf("%dkB", $filesize / 1024); + } elseif ($filesize > 0) { + $filesize += 103 - ($filesize % 103); + $fs = sprintf("%.1fkB", $filesize / 1024.0); + } else { + $fs = " -"; // XXX center instead + } + $data['data'][] = array($channel, $pkg, "$inst_version ($inst_state)", "$version ($state)", $fs); + } + if (isset($options['channelinfo'])) { + if (empty($data['data'])) { + unset($data['headline']); + if (count($inst) == 0) { + $data['data'] = '(no packages installed)'; + } else { + $data['data'] = '(no upgrades available)'; + } + } + $this->ui->outputData($data, $command); + } else { + if (empty($data['data'])) { + $this->ui->outputData('Channel ' . $channel . ': No upgrades available'); + } else { + $this->ui->outputData($data, $command); + } + } + } + $this->config->set('default_channel', $savechannel); + return true; + } + + // }}} + // {{{ doClearCache() + + function doClearCache($command, $options, $params) + { + $cache_dir = $this->config->get('cache_dir'); + $verbose = $this->config->get('verbose'); + $output = ''; + if (!file_exists($cache_dir) || !is_dir($cache_dir)) { + return $this->raiseError("$cache_dir does not exist or is not a directory"); + } + if (!($dp = @opendir($cache_dir))) { + return $this->raiseError("opendir($cache_dir) failed: $php_errormsg"); + } + if ($verbose >= 1) { + $output .= "reading directory $cache_dir\n"; + } + $num = 0; + while ($ent = readdir($dp)) { + if (preg_match('/^xmlrpc_cache_[a-z0-9]{32}\\z/', $ent) || + preg_match('/rest.cache(file|id)\\z/', $ent)) { + $path = $cache_dir . DIRECTORY_SEPARATOR . $ent; + if (file_exists($path)) { + $ok = @unlink($path); + } else { + $ok = false; + $php_errormsg = ''; + } + if ($ok) { + if ($verbose >= 2) { + $output .= "deleted $path\n"; + } + $num++; + } elseif ($verbose >= 1) { + $output .= "failed to delete $path $php_errormsg\n"; + } + } + } + closedir($dp); + if ($verbose >= 1) { + $output .= "$num cache entries cleared\n"; + } + $this->ui->outputData(rtrim($output), $command); + return $num; + } + + // }}} +} + +?> diff --git a/PEAR/Command/Remote.xml b/PEAR/Command/Remote.xml new file mode 100644 index 0000000..d06f222 --- /dev/null +++ b/PEAR/Command/Remote.xml @@ -0,0 +1,108 @@ + + + Information About Remote Packages + doRemoteInfo + ri + + <package> +Get details on a package from the server. + + + List Available Upgrades + doListUpgrades + lu + + [preferred_state] +List releases on the server of packages you have installed where +a newer version is available with the same release state (stable etc.) +or the state passed as the second parameter. + + + List Remote Packages + doRemoteList + rl + + + c + specify a channel other than the default channel + CHAN + + + i + output fully channel-aware data, even on failure + + + +Lists the packages available on the configured server along with the +latest stable release of each package. + + + Search remote package database + doSearch + sp + + + c + specify a channel other than the default channel + CHAN + + + a + search packages from all known channels + + + i + output fully channel-aware data, even on failure + + + [packagename] [packageinfo] +Lists all packages which match the search parameters. The first +parameter is a fragment of a packagename. The default channel +will be used unless explicitly overridden. The second parameter +will be used to match any portion of the summary/description + + + List All Packages + doListAll + la + + + c + specify a channel other than the default channel + CHAN + + + i + output fully channel-aware data, even on failure + + + +Lists the packages available on the configured server along with the +latest stable release of each package. + + + Download Package + doDownload + d + + + Z + download an uncompressed (.tar) file + + + <package>... +Download package tarballs. The files will be named as suggested by the +server, for example if you download the DB package and the latest stable +version of DB is 1.6.5, the downloaded file will be DB-1.6.5.tgz. + + + Clear Web Services Cache + doClearCache + cc + + +Clear the XML-RPC/REST cache. See also the cache_ttl configuration +parameter. + + + \ No newline at end of file diff --git a/PEAR/Command/Test.php b/PEAR/Command/Test.php new file mode 100644 index 0000000..6c450f8 --- /dev/null +++ b/PEAR/Command/Test.php @@ -0,0 +1,330 @@ + + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Test.php,v 1.23 2007/06/13 17:46:36 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +/** + * PEAR commands for login/logout + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ + +class PEAR_Command_Test extends PEAR_Command_Common +{ + // {{{ properties + + var $commands = array( + 'run-tests' => array( + 'summary' => 'Run Regression Tests', + 'function' => 'doRunTests', + 'shortcut' => 'rt', + 'options' => array( + 'recur' => array( + 'shortopt' => 'r', + 'doc' => 'Run tests in child directories, recursively. 4 dirs deep maximum', + ), + 'ini' => array( + 'shortopt' => 'i', + 'doc' => 'actual string of settings to pass to php in format " -d setting=blah"', + 'arg' => 'SETTINGS' + ), + 'realtimelog' => array( + 'shortopt' => 'l', + 'doc' => 'Log test runs/results as they are run', + ), + 'quiet' => array( + 'shortopt' => 'q', + 'doc' => 'Only display detail for failed tests', + ), + 'simple' => array( + 'shortopt' => 's', + 'doc' => 'Display simple output for all tests', + ), + 'package' => array( + 'shortopt' => 'p', + 'doc' => 'Treat parameters as installed packages from which to run tests', + ), + 'phpunit' => array( + 'shortopt' => 'u', + 'doc' => 'Search parameters for AllTests.php, and use that to run phpunit-based tests +If none is found, all .phpt tests will be tried instead.', + ), + 'tapoutput' => array( + 'shortopt' => 't', + 'doc' => 'Output run-tests.log in TAP-compliant format', + ), + 'cgi' => array( + 'shortopt' => 'c', + 'doc' => 'CGI php executable (needed for tests with POST/GET section)', + 'arg' => 'PHPCGI', + ), + 'coverage' => array( + 'shortopt' => 'x', + 'doc' => 'Generate a code coverage report (requires Xdebug 2.0.0+)', + ), + ), + 'doc' => '[testfile|dir ...] +Run regression tests with PHP\'s regression testing script (run-tests.php).', + ), + ); + + var $output; + + // }}} + // {{{ constructor + + /** + * PEAR_Command_Test constructor. + * + * @access public + */ + function PEAR_Command_Test(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + // }}} + // {{{ doRunTests() + + function doRunTests($command, $options, $params) + { + if (isset($options['phpunit']) && isset($options['tapoutput'])) { + return $this->raiseError('ERROR: cannot use both --phpunit and --tapoutput at the same time'); + } + require_once 'PEAR/Common.php'; + require_once 'System.php'; + $log = new PEAR_Common; + $log->ui = &$this->ui; // slightly hacky, but it will work + $tests = array(); + $depth = isset($options['recur']) ? 4 : 1; + + if (!count($params)) { + $params[] = '.'; + } + if (isset($options['package'])) { + $oldparams = $params; + $params = array(); + $reg = &$this->config->getRegistry(); + foreach ($oldparams as $param) { + $pname = $reg->parsePackageName($param, $this->config->get('default_channel')); + if (PEAR::isError($pname)) { + return $this->raiseError($pname); + } + + $package = &$reg->getPackage($pname['package'], $pname['channel']); + if (!$package) { + return PEAR::raiseError('Unknown package "' . + $reg->parsedPackageNameToString($pname) . '"'); + } + + $filelist = $package->getFilelist(); + foreach ($filelist as $name => $atts) { + if (isset($atts['role']) && $atts['role'] != 'test') { + continue; + } + + if (isset($options['phpunit']) && preg_match('/AllTests\.php\\z/i', $name)) { + $params = array($atts['installed_as']); + break; + } elseif (!preg_match('/\.phpt\\z/', $name)) { + continue; + } + $params[] = $atts['installed_as']; + } + } + } + + foreach ($params as $p) { + if (is_dir($p)) { + if (isset($options['phpunit'])) { + $dir = System::find(array($p, '-type', 'f', + '-maxdepth', $depth, + '-name', 'AllTests.php')); + if (count($dir)) { + $tests = $dir; + break; + } + } + $dir = System::find(array($p, '-type', 'f', + '-maxdepth', $depth, + '-name', '*.phpt')); + $tests = array_merge($tests, $dir); + } else { + if (isset($options['phpunit']) && preg_match('/AllTests\.php\\z/i', $p)) { + $tests = array($p); + break; + } + + if (!file_exists($p)) { + if (!preg_match('/\.phpt\\z/', $p)) { + $p .= '.phpt'; + } + $dir = System::find(array(dirname($p), '-type', 'f', + '-maxdepth', $depth, + '-name', $p)); + $tests = array_merge($tests, $dir); + } else { + $tests[] = $p; + } + } + } + + $ini_settings = ''; + if (isset($options['ini'])) { + $ini_settings .= $options['ini']; + } + + if (isset($_ENV['TEST_PHP_INCLUDE_PATH'])) { + $ini_settings .= " -d include_path={$_ENV['TEST_PHP_INCLUDE_PATH']}"; + } + + if ($ini_settings) { + $this->ui->outputData('Using INI settings: "' . $ini_settings . '"'); + } + $skipped = $passed = $failed = array(); + $tests_count = count($tests); + $this->ui->outputData('Running ' . $tests_count . ' tests', $command); + $start = time(); + if (isset($options['realtimelog']) && file_exists('run-tests.log')) { + unlink('run-tests.log'); + } + + if (isset($options['tapoutput'])) { + $tap = '1..' . $tests_count . "\n"; + } + + require_once 'PEAR/RunTest.php'; + $run = new PEAR_RunTest($log, $options); + $run->tests_count = $tests_count; + + if (isset($options['coverage']) && extension_loaded('xdebug')){ + $run->xdebug_loaded = true; + } else { + $run->xdebug_loaded = false; + } + + $j = $i = 1; + foreach ($tests as $t) { + if (isset($options['realtimelog'])) { + $fp = @fopen('run-tests.log', 'a'); + if ($fp) { + fwrite($fp, "Running test [$i / $tests_count] $t..."); + fclose($fp); + } + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (isset($options['phpunit'])) { + $result = $run->runPHPUnit($t, $ini_settings); + } else { + $result = $run->run($t, $ini_settings, $j); + } + PEAR::staticPopErrorHandling(); + if (PEAR::isError($result)) { + $this->ui->log($result->getMessage()); + continue; + } + + if (isset($options['tapoutput'])) { + $tap .= $result[0] . ' ' . $i . $result[1] . "\n"; + continue; + } + + if (isset($options['realtimelog'])) { + $fp = @fopen('run-tests.log', 'a'); + if ($fp) { + fwrite($fp, "$result\n"); + fclose($fp); + } + } + + if ($result == 'FAILED') { + $failed[] = $t; + } + if ($result == 'PASSED') { + $passed[] = $t; + } + if ($result == 'SKIPPED') { + $skipped[] = $t; + } + + $j++; + } + + $total = date('i:s', time() - $start); + if (isset($options['tapoutput'])) { + $fp = @fopen('run-tests.log', 'w'); + if ($fp) { + fwrite($fp, $tap, strlen($tap)); + fclose($fp); + $this->ui->outputData('wrote TAP-format log to "' .realpath('run-tests.log') . + '"', $command); + } + } else { + if (count($failed)) { + $output = "TOTAL TIME: $total\n"; + $output .= count($passed) . " PASSED TESTS\n"; + $output .= count($skipped) . " SKIPPED TESTS\n"; + $output .= count($failed) . " FAILED TESTS:\n"; + foreach ($failed as $failure) { + $output .= $failure . "\n"; + } + + $mode = isset($options['realtimelog']) ? 'a' : 'w'; + $fp = @fopen('run-tests.log', $mode); + + if ($fp) { + fwrite($fp, $output, strlen($output)); + fclose($fp); + $this->ui->outputData('wrote log to "' . realpath('run-tests.log') . '"', $command); + } + } elseif (file_exists('run-tests.log') && !is_dir('run-tests.log')) { + @unlink('run-tests.log'); + } + } + $this->ui->outputData('TOTAL TIME: ' . $total); + $this->ui->outputData(count($passed) . ' PASSED TESTS', $command); + $this->ui->outputData(count($skipped) . ' SKIPPED TESTS', $command); + if (count($failed)) { + $this->ui->outputData(count($failed) . ' FAILED TESTS:', $command); + foreach ($failed as $failure) { + $this->ui->outputData($failure, $command); + } + } + + return true; + } + // }}} +} \ No newline at end of file diff --git a/PEAR/Command/Test.xml b/PEAR/Command/Test.xml new file mode 100644 index 0000000..68e8f53 --- /dev/null +++ b/PEAR/Command/Test.xml @@ -0,0 +1,54 @@ + + + Run Regression Tests + doRunTests + rt + + + r + Run tests in child directories, recursively. 4 dirs deep maximum + + + i + actual string of settings to pass to php in format " -d setting=blah" + SETTINGS + + + l + Log test runs/results as they are run + + + q + Only display detail for failed tests + + + s + Display simple output for all tests + + + p + Treat parameters as installed packages from which to run tests + + + u + Search parameters for AllTests.php, and use that to run phpunit-based tests. +If none is found, all .phpt tests will be tried instead. + + + t + Output run-tests.log in TAP-compliant format + + + c + CGI php executable (needed for tests with POST/GET section) + PHPCGI + + + x + Generate a code coverage report (requires Xdebug 2.0.0+) + + + [testfile|dir ...] +Run regression tests with PHP's regression testing script (run-tests.php). + + \ No newline at end of file diff --git a/PEAR/Common.php b/PEAR/Common.php new file mode 100644 index 0000000..0d5f3d1 --- /dev/null +++ b/PEAR/Common.php @@ -0,0 +1,1126 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Common.php,v 1.159 2007/06/10 04:16:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1.0 + * @deprecated File deprecated since Release 1.4.0a1 + */ + +/** + * Include error handling + */ +require_once 'PEAR.php'; + +// {{{ constants and globals + +/** + * PEAR_Common error when an invalid PHP file is passed to PEAR_Common::analyzeSourceCode() + */ +define('PEAR_COMMON_ERROR_INVALIDPHP', 1); +define('_PEAR_COMMON_PACKAGE_NAME_PREG', '[A-Za-z][a-zA-Z0-9_]+'); +define('PEAR_COMMON_PACKAGE_NAME_PREG', '/^' . _PEAR_COMMON_PACKAGE_NAME_PREG . '\\z/'); + +// this should allow: 1, 1.0, 1.0RC1, 1.0dev, 1.0dev123234234234, 1.0a1, 1.0b1, 1.0pl1 +define('_PEAR_COMMON_PACKAGE_VERSION_PREG', '\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?'); +define('PEAR_COMMON_PACKAGE_VERSION_PREG', '/^' . _PEAR_COMMON_PACKAGE_VERSION_PREG . '\\z/i'); + +// XXX far from perfect :-) +define('_PEAR_COMMON_PACKAGE_DOWNLOAD_PREG', '(' . _PEAR_COMMON_PACKAGE_NAME_PREG . + ')(-([.0-9a-zA-Z]+))?'); +define('PEAR_COMMON_PACKAGE_DOWNLOAD_PREG', '/^' . _PEAR_COMMON_PACKAGE_DOWNLOAD_PREG . + '\\z/'); + +define('_PEAR_CHANNELS_NAME_PREG', '[A-Za-z][a-zA-Z0-9\.]+'); +define('PEAR_CHANNELS_NAME_PREG', '/^' . _PEAR_CHANNELS_NAME_PREG . '\\z/'); + +// this should allow any dns or IP address, plus a path - NO UNDERSCORES ALLOWED +define('_PEAR_CHANNELS_SERVER_PREG', '[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(\/[a-zA-Z0-9\-]+)*'); +define('PEAR_CHANNELS_SERVER_PREG', '/^' . _PEAR_CHANNELS_SERVER_PREG . '\\z/i'); + +define('_PEAR_CHANNELS_PACKAGE_PREG', '(' ._PEAR_CHANNELS_SERVER_PREG . ')\/(' + . _PEAR_COMMON_PACKAGE_NAME_PREG . ')'); +define('PEAR_CHANNELS_PACKAGE_PREG', '/^' . _PEAR_CHANNELS_PACKAGE_PREG . '\\z/i'); + +define('_PEAR_COMMON_CHANNEL_DOWNLOAD_PREG', '(' . _PEAR_CHANNELS_NAME_PREG . ')::(' + . _PEAR_COMMON_PACKAGE_NAME_PREG . ')(-([.0-9a-zA-Z]+))?'); +define('PEAR_COMMON_CHANNEL_DOWNLOAD_PREG', '/^' . _PEAR_COMMON_CHANNEL_DOWNLOAD_PREG . '\\z/'); + +/** + * List of temporary files and directories registered by + * PEAR_Common::addTempFile(). + * @var array + */ +$GLOBALS['_PEAR_Common_tempfiles'] = array(); + +/** + * Valid maintainer roles + * @var array + */ +$GLOBALS['_PEAR_Common_maintainer_roles'] = array('lead','developer','contributor','helper'); + +/** + * Valid release states + * @var array + */ +$GLOBALS['_PEAR_Common_release_states'] = array('alpha','beta','stable','snapshot','devel'); + +/** + * Valid dependency types + * @var array + */ +$GLOBALS['_PEAR_Common_dependency_types'] = array('pkg','ext','php','prog','ldlib','rtlib','os','websrv','sapi'); + +/** + * Valid dependency relations + * @var array + */ +$GLOBALS['_PEAR_Common_dependency_relations'] = array('has','eq','lt','le','gt','ge','not', 'ne'); + +/** + * Valid file roles + * @var array + */ +$GLOBALS['_PEAR_Common_file_roles'] = array('php','ext','test','doc','data','src','script'); + +/** + * Valid replacement types + * @var array + */ +$GLOBALS['_PEAR_Common_replacement_types'] = array('php-const', 'pear-config', 'package-info'); + +/** + * Valid "provide" types + * @var array + */ +$GLOBALS['_PEAR_Common_provide_types'] = array('ext', 'prog', 'class', 'function', 'feature', 'api'); + +/** + * Valid "provide" types + * @var array + */ +$GLOBALS['_PEAR_Common_script_phases'] = array('pre-install', 'post-install', 'pre-uninstall', 'post-uninstall', 'pre-build', 'post-build', 'pre-configure', 'post-configure', 'pre-setup', 'post-setup'); + +// }}} + +/** + * Class providing common functionality for PEAR administration classes. + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + * @deprecated This class will disappear, and its components will be spread + * into smaller classes, like the AT&T breakup, as of Release 1.4.0a1 + */ +class PEAR_Common extends PEAR +{ + // {{{ properties + + /** stack of elements, gives some sort of XML context */ + var $element_stack = array(); + + /** name of currently parsed XML element */ + var $current_element; + + /** array of attributes of the currently parsed XML element */ + var $current_attributes = array(); + + /** assoc with information about a package */ + var $pkginfo = array(); + + /** + * User Interface object (PEAR_Frontend_* class). If null, + * the log() method uses print. + * @var object + */ + var $ui = null; + + /** + * Configuration object (PEAR_Config). + * @var PEAR_Config + */ + var $config = null; + + var $current_path = null; + + /** + * PEAR_SourceAnalyzer instance + * @var object + */ + var $source_analyzer = null; + /** + * Flag variable used to mark a valid package file + * @var boolean + * @access private + */ + var $_validPackageFile; + + // }}} + + // {{{ constructor + + /** + * PEAR_Common constructor + * + * @access public + */ + function PEAR_Common() + { + parent::PEAR(); + $this->config = &PEAR_Config::singleton(); + $this->debug = $this->config->get('verbose'); + } + + // }}} + // {{{ destructor + + /** + * PEAR_Common destructor + * + * @access private + */ + function _PEAR_Common() + { + // doesn't work due to bug #14744 + //$tempfiles = $this->_tempfiles; + $tempfiles =& $GLOBALS['_PEAR_Common_tempfiles']; + while ($file = array_shift($tempfiles)) { + if (@is_dir($file)) { + if (!class_exists('System')) { + require_once 'System.php'; + } + System::rm(array('-rf', $file)); + } elseif (file_exists($file)) { + unlink($file); + } + } + } + + // }}} + // {{{ addTempFile() + + /** + * Register a temporary file or directory. When the destructor is + * executed, all registered temporary files and directories are + * removed. + * + * @param string $file name of file or directory + * + * @return void + * + * @access public + */ + function addTempFile($file) + { + if (!class_exists('PEAR_Frontend')) { + require_once 'PEAR/Frontend.php'; + } + PEAR_Frontend::addTempFile($file); + } + + // }}} + // {{{ mkDirHier() + + /** + * Wrapper to System::mkDir(), creates a directory as well as + * any necessary parent directories. + * + * @param string $dir directory name + * + * @return bool TRUE on success, or a PEAR error + * + * @access public + */ + function mkDirHier($dir) + { + $this->log(2, "+ create dir $dir"); + if (!class_exists('System')) { + require_once 'System.php'; + } + return System::mkDir(array('-p', $dir)); + } + + // }}} + // {{{ log() + + /** + * Logging method. + * + * @param int $level log level (0 is quiet, higher is noisier) + * @param string $msg message to write to the log + * + * @return void + * + * @access public + * @static + */ + function log($level, $msg, $append_crlf = true) + { + if ($this->debug >= $level) { + if (!class_exists('PEAR_Frontend')) { + require_once 'PEAR/Frontend.php'; + } + $ui = &PEAR_Frontend::singleton(); + if (is_a($ui, 'PEAR_Frontend')) { + $ui->log($msg, $append_crlf); + } else { + print "$msg\n"; + } + } + } + + // }}} + // {{{ mkTempDir() + + /** + * Create and register a temporary directory. + * + * @param string $tmpdir (optional) Directory to use as tmpdir. + * Will use system defaults (for example + * /tmp or c:\windows\temp) if not specified + * + * @return string name of created directory + * + * @access public + */ + function mkTempDir($tmpdir = '') + { + if ($tmpdir) { + $topt = array('-t', $tmpdir); + } else { + $topt = array(); + } + $topt = array_merge($topt, array('-d', 'pear')); + if (!class_exists('System')) { + require_once 'System.php'; + } + if (!$tmpdir = System::mktemp($topt)) { + return false; + } + $this->addTempFile($tmpdir); + return $tmpdir; + } + + // }}} + // {{{ setFrontendObject() + + /** + * Set object that represents the frontend to be used. + * + * @param object Reference of the frontend object + * @return void + * @access public + */ + function setFrontendObject(&$ui) + { + $this->ui = &$ui; + } + + // }}} + + // {{{ infoFromTgzFile() + + /** + * Returns information about a package file. Expects the name of + * a gzipped tar file as input. + * + * @param string $file name of .tgz file + * + * @return array array with package information + * + * @access public + * @deprecated use PEAR_PackageFile->fromTgzFile() instead + * + */ + function infoFromTgzFile($file) + { + $packagefile = &new PEAR_PackageFile($this->config); + $pf = &$packagefile->fromTgzFile($file, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pf)) { + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + $e = $this->raiseError($error['message'], $error['code'], null, null, $error); + } + } + return $pf; + } + return $this->_postProcessValidPackagexml($pf); + } + + // }}} + // {{{ infoFromDescriptionFile() + + /** + * Returns information about a package file. Expects the name of + * a package xml file as input. + * + * @param string $descfile name of package xml file + * + * @return array array with package information + * + * @access public + * @deprecated use PEAR_PackageFile->fromPackageFile() instead + * + */ + function infoFromDescriptionFile($descfile) + { + $packagefile = &new PEAR_PackageFile($this->config); + $pf = &$packagefile->fromPackageFile($descfile, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pf)) { + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + $e = $this->raiseError($error['message'], $error['code'], null, null, $error); + } + } + return $pf; + } + return $this->_postProcessValidPackagexml($pf); + } + + // }}} + // {{{ infoFromString() + + /** + * Returns information about a package file. Expects the contents + * of a package xml file as input. + * + * @param string $data contents of package.xml file + * + * @return array array with package information + * + * @access public + * @deprecated use PEAR_PackageFile->fromXmlstring() instead + * + */ + function infoFromString($data) + { + $packagefile = &new PEAR_PackageFile($this->config); + $pf = &$packagefile->fromXmlString($data, PEAR_VALIDATE_NORMAL, false); + if (PEAR::isError($pf)) { + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + $e = $this->raiseError($error['message'], $error['code'], null, null, $error); + } + } + return $pf; + } + return $this->_postProcessValidPackagexml($pf); + } + // }}} + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @return array + */ + function _postProcessValidPackagexml(&$pf) + { + if (is_a($pf, 'PEAR_PackageFile_v2')) { + // sort of make this into a package.xml 1.0-style array + // changelog is not converted to old format. + $arr = $pf->toArray(true); + $arr = array_merge($arr, $arr['old']); + unset($arr['old']); + unset($arr['xsdversion']); + unset($arr['contents']); + unset($arr['compatible']); + unset($arr['channel']); + unset($arr['uri']); + unset($arr['dependencies']); + unset($arr['phprelease']); + unset($arr['extsrcrelease']); + unset($arr['zendextsrcrelease']); + unset($arr['extbinrelease']); + unset($arr['zendextbinrelease']); + unset($arr['bundle']); + unset($arr['lead']); + unset($arr['developer']); + unset($arr['helper']); + unset($arr['contributor']); + $arr['filelist'] = $pf->getFilelist(); + $this->pkginfo = $arr; + return $arr; + } else { + $this->pkginfo = $pf->toArray(); + return $this->pkginfo; + } + } + // {{{ infoFromAny() + + /** + * Returns package information from different sources + * + * This method is able to extract information about a package + * from a .tgz archive or from a XML package definition file. + * + * @access public + * @param string Filename of the source ('package.xml', '.tgz') + * @return string + * @deprecated use PEAR_PackageFile->fromAnyFile() instead + */ + function infoFromAny($info) + { + if (is_string($info) && file_exists($info)) { + $packagefile = &new PEAR_PackageFile($this->config); + $pf = &$packagefile->fromAnyFile($info, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pf)) { + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + $e = $this->raiseError($error['message'], $error['code'], null, null, $error); + } + } + return $pf; + } + return $this->_postProcessValidPackagexml($pf); + } + return $info; + } + + // }}} + // {{{ xmlFromInfo() + + /** + * Return an XML document based on the package info (as returned + * by the PEAR_Common::infoFrom* methods). + * + * @param array $pkginfo package info + * + * @return string XML data + * + * @access public + * @deprecated use a PEAR_PackageFile_v* object's generator instead + */ + function xmlFromInfo($pkginfo) + { + $config = &PEAR_Config::singleton(); + $packagefile = &new PEAR_PackageFile($config); + $pf = &$packagefile->fromArray($pkginfo); + $gen = &$pf->getDefaultGenerator(); + return $gen->toXml(PEAR_VALIDATE_PACKAGING); + } + + // }}} + // {{{ validatePackageInfo() + + /** + * Validate XML package definition file. + * + * @param string $info Filename of the package archive or of the + * package definition file + * @param array $errors Array that will contain the errors + * @param array $warnings Array that will contain the warnings + * @param string $dir_prefix (optional) directory where source files + * may be found, or empty if they are not available + * @access public + * @return boolean + * @deprecated use the validation of PEAR_PackageFile objects + */ + function validatePackageInfo($info, &$errors, &$warnings, $dir_prefix = '') + { + $config = &PEAR_Config::singleton(); + $packagefile = &new PEAR_PackageFile($config); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (strpos($info, 'fromXmlString($info, PEAR_VALIDATE_NORMAL, ''); + } else { + $pf = &$packagefile->fromAnyFile($info, PEAR_VALIDATE_NORMAL); + } + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pf)) { + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + if ($error['level'] == 'error') { + $errors[] = $error['message']; + } else { + $warnings[] = $error['message']; + } + } + } + return false; + } + return true; + } + + // }}} + // {{{ buildProvidesArray() + + /** + * Build a "provides" array from data returned by + * analyzeSourceCode(). The format of the built array is like + * this: + * + * array( + * 'class;MyClass' => 'array('type' => 'class', 'name' => 'MyClass'), + * ... + * ) + * + * + * @param array $srcinfo array with information about a source file + * as returned by the analyzeSourceCode() method. + * + * @return void + * + * @access public + * + */ + function buildProvidesArray($srcinfo) + { + $file = basename($srcinfo['source_file']); + $pn = ''; + if (isset($this->_packageName)) { + $pn = $this->_packageName; + } + $pnl = strlen($pn); + foreach ($srcinfo['declared_classes'] as $class) { + $key = "class;$class"; + if (isset($this->pkginfo['provides'][$key])) { + continue; + } + $this->pkginfo['provides'][$key] = + array('file'=> $file, 'type' => 'class', 'name' => $class); + if (isset($srcinfo['inheritance'][$class])) { + $this->pkginfo['provides'][$key]['extends'] = + $srcinfo['inheritance'][$class]; + } + } + foreach ($srcinfo['declared_methods'] as $class => $methods) { + foreach ($methods as $method) { + $function = "$class::$method"; + $key = "function;$function"; + if ($method{0} == '_' || !strcasecmp($method, $class) || + isset($this->pkginfo['provides'][$key])) { + continue; + } + $this->pkginfo['provides'][$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + } + + foreach ($srcinfo['declared_functions'] as $function) { + $key = "function;$function"; + if ($function{0} == '_' || isset($this->pkginfo['provides'][$key])) { + continue; + } + if (!strstr($function, '::') && strncasecmp($function, $pn, $pnl)) { + $warnings[] = "in1 " . $file . ": function \"$function\" not prefixed with package name \"$pn\""; + } + $this->pkginfo['provides'][$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + } + + // }}} + // {{{ analyzeSourceCode() + + /** + * Analyze the source code of the given PHP file + * + * @param string Filename of the PHP file + * @return mixed + * @access public + */ + function analyzeSourceCode($file) + { + if (!function_exists("token_get_all")) { + return false; + } + if (!defined('T_DOC_COMMENT')) { + define('T_DOC_COMMENT', T_COMMENT); + } + if (!defined('T_INTERFACE')) { + define('T_INTERFACE', -1); + } + if (!defined('T_IMPLEMENTS')) { + define('T_IMPLEMENTS', -1); + } + if (!$fp = @fopen($file, "r")) { + return false; + } + fclose($fp); + $contents = file_get_contents($file); + $tokens = token_get_all($contents); +/* + for ($i = 0; $i < sizeof($tokens); $i++) { + @list($token, $data) = $tokens[$i]; + if (is_string($token)) { + var_dump($token); + } else { + print token_name($token) . ' '; + var_dump(rtrim($data)); + } + } +*/ + $look_for = 0; + $paren_level = 0; + $bracket_level = 0; + $brace_level = 0; + $lastphpdoc = ''; + $current_class = ''; + $current_interface = ''; + $current_class_level = -1; + $current_function = ''; + $current_function_level = -1; + $declared_classes = array(); + $declared_interfaces = array(); + $declared_functions = array(); + $declared_methods = array(); + $used_classes = array(); + $used_functions = array(); + $extends = array(); + $implements = array(); + $nodeps = array(); + $inquote = false; + $interface = false; + for ($i = 0; $i < sizeof($tokens); $i++) { + if (is_array($tokens[$i])) { + list($token, $data) = $tokens[$i]; + } else { + $token = $tokens[$i]; + $data = ''; + } + if ($inquote) { + if ($token != '"') { + continue; + } else { + $inquote = false; + continue; + } + } + switch ($token) { + case T_WHITESPACE: + continue; + case ';': + if ($interface) { + $current_function = ''; + $current_function_level = -1; + } + break; + case '"': + $inquote = true; + break; + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + case '{': $brace_level++; continue 2; + case '}': + $brace_level--; + if ($current_class_level == $brace_level) { + $current_class = ''; + $current_class_level = -1; + } + if ($current_function_level == $brace_level) { + $current_function = ''; + $current_function_level = -1; + } + continue 2; + case '[': $bracket_level++; continue 2; + case ']': $bracket_level--; continue 2; + case '(': $paren_level++; continue 2; + case ')': $paren_level--; continue 2; + case T_INTERFACE: + $interface = true; + case T_CLASS: + if (($current_class_level != -1) || ($current_function_level != -1)) { + PEAR::raiseError("Parser error: invalid PHP found in file \"$file\"", + PEAR_COMMON_ERROR_INVALIDPHP); + return false; + } + case T_FUNCTION: + case T_NEW: + case T_EXTENDS: + case T_IMPLEMENTS: + $look_for = $token; + continue 2; + case T_STRING: + if (version_compare(zend_version(), '2.0', '<')) { + if (in_array(strtolower($data), + array('public', 'private', 'protected', 'abstract', + 'interface', 'implements', 'throw') + )) { + PEAR::raiseError('Error: PHP5 token encountered in ' . $file . + 'packaging should be done in PHP 5'); + return false; + } + } + if ($look_for == T_CLASS) { + $current_class = $data; + $current_class_level = $brace_level; + $declared_classes[] = $current_class; + } elseif ($look_for == T_INTERFACE) { + $current_interface = $data; + $current_class_level = $brace_level; + $declared_interfaces[] = $current_interface; + } elseif ($look_for == T_IMPLEMENTS) { + $implements[$current_class] = $data; + } elseif ($look_for == T_EXTENDS) { + $extends[$current_class] = $data; + } elseif ($look_for == T_FUNCTION) { + if ($current_class) { + $current_function = "$current_class::$data"; + $declared_methods[$current_class][] = $data; + } elseif ($current_interface) { + $current_function = "$current_interface::$data"; + $declared_methods[$current_interface][] = $data; + } else { + $current_function = $data; + $declared_functions[] = $current_function; + } + $current_function_level = $brace_level; + $m = array(); + } elseif ($look_for == T_NEW) { + $used_classes[$data] = true; + } + $look_for = 0; + continue 2; + case T_VARIABLE: + $look_for = 0; + continue 2; + case T_DOC_COMMENT: + case T_COMMENT: + if (preg_match('!^/\*\*\s!', $data)) { + $lastphpdoc = $data; + if (preg_match_all('/@nodep\s+(\S+)/', $lastphpdoc, $m)) { + $nodeps = array_merge($nodeps, $m[1]); + } + } + continue 2; + case T_DOUBLE_COLON: + if (!($tokens[$i - 1][0] == T_WHITESPACE || $tokens[$i - 1][0] == T_STRING)) { + PEAR::raiseError("Parser error: invalid PHP found in file \"$file\"", + PEAR_COMMON_ERROR_INVALIDPHP); + return false; + } + $class = $tokens[$i - 1][1]; + if (strtolower($class) != 'parent') { + $used_classes[$class] = true; + } + continue 2; + } + } + return array( + "source_file" => $file, + "declared_classes" => $declared_classes, + "declared_interfaces" => $declared_interfaces, + "declared_methods" => $declared_methods, + "declared_functions" => $declared_functions, + "used_classes" => array_diff(array_keys($used_classes), $nodeps), + "inheritance" => $extends, + "implements" => $implements, + ); + } + + // }}} + // {{{ betterStates() + + /** + * Return an array containing all of the states that are more stable than + * or equal to the passed in state + * + * @param string Release state + * @param boolean Determines whether to include $state in the list + * @return false|array False if $state is not a valid release state + */ + function betterStates($state, $include = false) + { + static $states = array('snapshot', 'devel', 'alpha', 'beta', 'stable'); + $i = array_search($state, $states); + if ($i === false) { + return false; + } + if ($include) { + $i--; + } + return array_slice($states, $i + 1); + } + + // }}} + // {{{ detectDependencies() + + function detectDependencies($any, $status_callback = null) + { + if (!function_exists("token_get_all")) { + return false; + } + if (PEAR::isError($info = $this->infoFromAny($any))) { + return $this->raiseError($info); + } + if (!is_array($info)) { + return false; + } + $deps = array(); + $used_c = $decl_c = $decl_f = $decl_m = array(); + foreach ($info['filelist'] as $file => $fa) { + $tmp = $this->analyzeSourceCode($file); + $used_c = @array_merge($used_c, $tmp['used_classes']); + $decl_c = @array_merge($decl_c, $tmp['declared_classes']); + $decl_f = @array_merge($decl_f, $tmp['declared_functions']); + $decl_m = @array_merge($decl_m, $tmp['declared_methods']); + $inheri = @array_merge($inheri, $tmp['inheritance']); + } + $used_c = array_unique($used_c); + $decl_c = array_unique($decl_c); + $undecl_c = array_diff($used_c, $decl_c); + return array('used_classes' => $used_c, + 'declared_classes' => $decl_c, + 'declared_methods' => $decl_m, + 'declared_functions' => $decl_f, + 'undeclared_classes' => $undecl_c, + 'inheritance' => $inheri, + ); + } + + // }}} + // {{{ getUserRoles() + + /** + * Get the valid roles for a PEAR package maintainer + * + * @return array + * @static + */ + function getUserRoles() + { + return $GLOBALS['_PEAR_Common_maintainer_roles']; + } + + // }}} + // {{{ getReleaseStates() + + /** + * Get the valid package release states of packages + * + * @return array + * @static + */ + function getReleaseStates() + { + return $GLOBALS['_PEAR_Common_release_states']; + } + + // }}} + // {{{ getDependencyTypes() + + /** + * Get the implemented dependency types (php, ext, pkg etc.) + * + * @return array + * @static + */ + function getDependencyTypes() + { + return $GLOBALS['_PEAR_Common_dependency_types']; + } + + // }}} + // {{{ getDependencyRelations() + + /** + * Get the implemented dependency relations (has, lt, ge etc.) + * + * @return array + * @static + */ + function getDependencyRelations() + { + return $GLOBALS['_PEAR_Common_dependency_relations']; + } + + // }}} + // {{{ getFileRoles() + + /** + * Get the implemented file roles + * + * @return array + * @static + */ + function getFileRoles() + { + return $GLOBALS['_PEAR_Common_file_roles']; + } + + // }}} + // {{{ getReplacementTypes() + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getReplacementTypes() + { + return $GLOBALS['_PEAR_Common_replacement_types']; + } + + // }}} + // {{{ getProvideTypes() + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getProvideTypes() + { + return $GLOBALS['_PEAR_Common_provide_types']; + } + + // }}} + // {{{ getScriptPhases() + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getScriptPhases() + { + return $GLOBALS['_PEAR_Common_script_phases']; + } + + // }}} + // {{{ validPackageName() + + /** + * Test whether a string contains a valid package name. + * + * @param string $name the package name to test + * + * @return bool + * + * @access public + */ + function validPackageName($name) + { + return (bool)preg_match(PEAR_COMMON_PACKAGE_NAME_PREG, $name); + } + + + // }}} + // {{{ validPackageVersion() + + /** + * Test whether a string contains a valid package version. + * + * @param string $ver the package version to test + * + * @return bool + * + * @access public + */ + function validPackageVersion($ver) + { + return (bool)preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $ver); + } + + + // }}} + + // {{{ downloadHttp() + + /** + * Download a file through HTTP. Considers suggested file name in + * Content-disposition: header and can run a callback function for + * different events. The callback will be called with two + * parameters: the callback type, and parameters. The implemented + * callback types are: + * + * 'setup' called at the very beginning, parameter is a UI object + * that should be used for all output + * 'message' the parameter is a string with an informational message + * 'saveas' may be used to save with a different file name, the + * parameter is the filename that is about to be used. + * If a 'saveas' callback returns a non-empty string, + * that file name will be used as the filename instead. + * Note that $save_dir will not be affected by this, only + * the basename of the file. + * 'start' download is starting, parameter is number of bytes + * that are expected, or -1 if unknown + * 'bytesread' parameter is the number of bytes read so far + * 'done' download is complete, parameter is the total number + * of bytes read + * 'connfailed' if the TCP connection fails, this callback is called + * with array(host,port,errno,errmsg) + * 'writefailed' if writing to disk fails, this callback is called + * with array(destfile,errmsg) + * + * If an HTTP proxy has been configured (http_proxy PEAR_Config + * setting), the proxy will be used. + * + * @param string $url the URL to download + * @param object $ui PEAR_Frontend_* instance + * @param object $config PEAR_Config instance + * @param string $save_dir (optional) directory to save file in + * @param mixed $callback (optional) function/method to call for status + * updates + * + * @return string Returns the full path of the downloaded file or a PEAR + * error on failure. If the error is caused by + * socket-related errors, the error object will + * have the fsockopen error code available through + * getCode(). + * + * @access public + * @deprecated in favor of PEAR_Downloader::downloadHttp() + */ + function downloadHttp($url, &$ui, $save_dir = '.', $callback = null) + { + if (!class_exists('PEAR_Downloader')) { + require_once 'PEAR/Downloader.php'; + } + return PEAR_Downloader::downloadHttp($url, $ui, $save_dir, $callback); + } + + // }}} + + /** + * @param string $path relative or absolute include path + * @return boolean + * @static + */ + function isIncludeable($path) + { + if (file_exists($path) && is_readable($path)) { + return true; + } + $ipath = explode(PATH_SEPARATOR, ini_get('include_path')); + foreach ($ipath as $include) { + $test = realpath($include . DIRECTORY_SEPARATOR . $path); + if (file_exists($test) && is_readable($test)) { + return true; + } + } + return false; + } +} +require_once 'PEAR/Config.php'; +require_once 'PEAR/PackageFile.php'; +?> \ No newline at end of file diff --git a/PEAR/Config.php b/PEAR/Config.php new file mode 100644 index 0000000..f9c9902 --- /dev/null +++ b/PEAR/Config.php @@ -0,0 +1,2108 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Config.php,v 1.137 2006/11/19 21:33:00 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * Required for error handling + */ +require_once 'PEAR.php'; +require_once 'PEAR/Registry.php'; +require_once 'PEAR/Installer/Role.php'; +require_once 'System.php'; +require_once 'PEAR/Remote.php'; + +/** + * Last created PEAR_Config instance. + * @var object + */ +$GLOBALS['_PEAR_Config_instance'] = null; +if (!defined('PEAR_INSTALL_DIR') || !PEAR_INSTALL_DIR) { + $PEAR_INSTALL_DIR = PHP_LIBDIR . DIRECTORY_SEPARATOR . 'pear'; +} else { + $PEAR_INSTALL_DIR = PEAR_INSTALL_DIR; +} + +// Below we define constants with default values for all configuration +// parameters except username/password. All of them can have their +// defaults set through environment variables. The reason we use the +// PHP_ prefix is for some security, PHP protects environment +// variables starting with PHP_*. + +// default channel and preferred mirror is based on whether we are invoked through +// the "pear" or the "pecl" command + +if (!defined('PEAR_RUNTYPE') || PEAR_RUNTYPE == 'pear') { + define('PEAR_CONFIG_DEFAULT_CHANNEL', 'pear.php.net'); +} else { + define('PEAR_CONFIG_DEFAULT_CHANNEL', 'pecl.php.net'); +} + +if (getenv('PHP_PEAR_SYSCONF_DIR')) { + define('PEAR_CONFIG_SYSCONFDIR', getenv('PHP_PEAR_SYSCONF_DIR')); +} elseif (getenv('SystemRoot')) { + define('PEAR_CONFIG_SYSCONFDIR', getenv('SystemRoot')); +} else { + define('PEAR_CONFIG_SYSCONFDIR', PHP_SYSCONFDIR); +} + +// Default for master_server +if (getenv('PHP_PEAR_MASTER_SERVER')) { + define('PEAR_CONFIG_DEFAULT_MASTER_SERVER', getenv('PHP_PEAR_MASTER_SERVER')); +} else { + define('PEAR_CONFIG_DEFAULT_MASTER_SERVER', 'pear.php.net'); +} + +// Default for http_proxy +if (getenv('PHP_PEAR_HTTP_PROXY')) { + define('PEAR_CONFIG_DEFAULT_HTTP_PROXY', getenv('PHP_PEAR_HTTP_PROXY')); +} elseif (getenv('http_proxy')) { + define('PEAR_CONFIG_DEFAULT_HTTP_PROXY', getenv('http_proxy')); +} else { + define('PEAR_CONFIG_DEFAULT_HTTP_PROXY', ''); +} + +// Default for php_dir +if (getenv('PHP_PEAR_INSTALL_DIR')) { + define('PEAR_CONFIG_DEFAULT_PHP_DIR', getenv('PHP_PEAR_INSTALL_DIR')); +} else { + if (file_exists($PEAR_INSTALL_DIR) && is_dir($PEAR_INSTALL_DIR)) { + define('PEAR_CONFIG_DEFAULT_PHP_DIR', + $PEAR_INSTALL_DIR); + } else { + define('PEAR_CONFIG_DEFAULT_PHP_DIR', $PEAR_INSTALL_DIR); + } +} + +// Default for ext_dir +if (getenv('PHP_PEAR_EXTENSION_DIR')) { + define('PEAR_CONFIG_DEFAULT_EXT_DIR', getenv('PHP_PEAR_EXTENSION_DIR')); +} else { + if (ini_get('extension_dir')) { + define('PEAR_CONFIG_DEFAULT_EXT_DIR', ini_get('extension_dir')); + } elseif (defined('PEAR_EXTENSION_DIR') && + file_exists(PEAR_EXTENSION_DIR) && is_dir(PEAR_EXTENSION_DIR)) { + define('PEAR_CONFIG_DEFAULT_EXT_DIR', PEAR_EXTENSION_DIR); + } elseif (defined('PHP_EXTENSION_DIR')) { + define('PEAR_CONFIG_DEFAULT_EXT_DIR', PHP_EXTENSION_DIR); + } else { + define('PEAR_CONFIG_DEFAULT_EXT_DIR', '.'); + } +} + +// Default for doc_dir +if (getenv('PHP_PEAR_DOC_DIR')) { + define('PEAR_CONFIG_DEFAULT_DOC_DIR', getenv('PHP_PEAR_DOC_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_DOC_DIR', + $PEAR_INSTALL_DIR.DIRECTORY_SEPARATOR.'docs'); +} + +// Default for bin_dir +if (getenv('PHP_PEAR_BIN_DIR')) { + define('PEAR_CONFIG_DEFAULT_BIN_DIR', getenv('PHP_PEAR_BIN_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_BIN_DIR', PHP_BINDIR); +} + +// Default for data_dir +if (getenv('PHP_PEAR_DATA_DIR')) { + define('PEAR_CONFIG_DEFAULT_DATA_DIR', getenv('PHP_PEAR_DATA_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_DATA_DIR', + $PEAR_INSTALL_DIR.DIRECTORY_SEPARATOR.'data'); +} + +// Default for test_dir +if (getenv('PHP_PEAR_TEST_DIR')) { + define('PEAR_CONFIG_DEFAULT_TEST_DIR', getenv('PHP_PEAR_TEST_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_TEST_DIR', + $PEAR_INSTALL_DIR.DIRECTORY_SEPARATOR.'tests'); +} + +// Default for temp_dir +if (getenv('PHP_PEAR_TEMP_DIR')) { + define('PEAR_CONFIG_DEFAULT_TEMP_DIR', getenv('PHP_PEAR_TEMP_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_TEMP_DIR', + System::tmpdir() . DIRECTORY_SEPARATOR . 'pear' . + DIRECTORY_SEPARATOR . 'temp'); +} + +// Default for cache_dir +if (getenv('PHP_PEAR_CACHE_DIR')) { + define('PEAR_CONFIG_DEFAULT_CACHE_DIR', getenv('PHP_PEAR_CACHE_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_CACHE_DIR', + System::tmpdir() . DIRECTORY_SEPARATOR . 'pear' . + DIRECTORY_SEPARATOR . 'cache'); +} + +// Default for download_dir +if (getenv('PHP_PEAR_DOWNLOAD_DIR')) { + define('PEAR_CONFIG_DEFAULT_DOWNLOAD_DIR', getenv('PHP_PEAR_DOWNLOAD_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_DOWNLOAD_DIR', + System::tmpdir() . DIRECTORY_SEPARATOR . 'pear' . + DIRECTORY_SEPARATOR . 'download'); +} + +// Default for php_bin +if (getenv('PHP_PEAR_PHP_BIN')) { + define('PEAR_CONFIG_DEFAULT_PHP_BIN', getenv('PHP_PEAR_PHP_BIN')); +} else { + define('PEAR_CONFIG_DEFAULT_PHP_BIN', PEAR_CONFIG_DEFAULT_BIN_DIR. + DIRECTORY_SEPARATOR.'php'.(OS_WINDOWS ? '.exe' : '')); +} + +// Default for verbose +if (getenv('PHP_PEAR_VERBOSE')) { + define('PEAR_CONFIG_DEFAULT_VERBOSE', getenv('PHP_PEAR_VERBOSE')); +} else { + define('PEAR_CONFIG_DEFAULT_VERBOSE', 1); +} + +// Default for preferred_state +if (getenv('PHP_PEAR_PREFERRED_STATE')) { + define('PEAR_CONFIG_DEFAULT_PREFERRED_STATE', getenv('PHP_PEAR_PREFERRED_STATE')); +} else { + define('PEAR_CONFIG_DEFAULT_PREFERRED_STATE', 'stable'); +} + +// Default for umask +if (getenv('PHP_PEAR_UMASK')) { + define('PEAR_CONFIG_DEFAULT_UMASK', getenv('PHP_PEAR_UMASK')); +} else { + define('PEAR_CONFIG_DEFAULT_UMASK', decoct(umask())); +} + +// Default for cache_ttl +if (getenv('PHP_PEAR_CACHE_TTL')) { + define('PEAR_CONFIG_DEFAULT_CACHE_TTL', getenv('PHP_PEAR_CACHE_TTL')); +} else { + define('PEAR_CONFIG_DEFAULT_CACHE_TTL', 3600); +} + +// Default for sig_type +if (getenv('PHP_PEAR_SIG_TYPE')) { + define('PEAR_CONFIG_DEFAULT_SIG_TYPE', getenv('PHP_PEAR_SIG_TYPE')); +} else { + define('PEAR_CONFIG_DEFAULT_SIG_TYPE', 'gpg'); +} + +// Default for sig_bin +if (getenv('PHP_PEAR_SIG_BIN')) { + define('PEAR_CONFIG_DEFAULT_SIG_BIN', getenv('PHP_PEAR_SIG_BIN')); +} else { + define('PEAR_CONFIG_DEFAULT_SIG_BIN', + System::which( + 'gpg', OS_WINDOWS ? 'c:\gnupg\gpg.exe' : '/usr/local/bin/gpg')); +} + +// Default for sig_keydir +if (getenv('PHP_PEAR_SIG_KEYDIR')) { + define('PEAR_CONFIG_DEFAULT_SIG_KEYDIR', getenv('PHP_PEAR_SIG_KEYDIR')); +} else { + define('PEAR_CONFIG_DEFAULT_SIG_KEYDIR', + PEAR_CONFIG_SYSCONFDIR . DIRECTORY_SEPARATOR . 'pearkeys'); +} + +/** + * This is a class for storing configuration data, keeping track of + * which are system-defined, user-defined or defaulted. + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Config extends PEAR +{ + // {{{ properties + + /** + * Array of config files used. + * + * @var array layer => config file + */ + var $files = array( + 'system' => '', + 'user' => '', + ); + + var $layers = array(); + + /** + * Configuration data, two-dimensional array where the first + * dimension is the config layer ('user', 'system' and 'default'), + * and the second dimension is keyname => value. + * + * The order in the first dimension is important! Earlier + * layers will shadow later ones when a config value is + * requested (if a 'user' value exists, it will be returned first, + * then 'system' and finally 'default'). + * + * @var array layer => array(keyname => value, ...) + */ + var $configuration = array( + 'user' => array(), + 'system' => array(), + 'default' => array(), + ); + + /** + * Configuration values that can be set for a channel + * + * All other configuration values can only have a global value + * @var array + * @access private + */ + var $_channelConfigInfo = array( + 'php_dir', 'ext_dir', 'doc_dir', 'bin_dir', 'data_dir', + 'test_dir', 'php_bin', 'username', 'password', 'verbose', + 'preferred_state', 'umask', 'preferred_mirror', + ); + + /** + * Channels that can be accessed + * @see setChannels() + * @var array + * @access private + */ + var $_channels = array('pear.php.net', 'pecl.php.net', '__uri'); + + /** + * This variable is used to control the directory values returned + * @see setInstallRoot(); + * @var string|false + * @access private + */ + var $_installRoot = false; + + /** + * If requested, this will always refer to the registry + * contained in php_dir + * @var PEAR_Registry + */ + var $_registry = array(); + + /** + * @var array + * @access private + */ + var $_regInitialized = array(); + + /** + * @var bool + * @access private + */ + var $_noRegistry = false; + + /** + * amount of errors found while parsing config + * @var integer + * @access private + */ + var $_errorsFound = 0; + var $_lastError = null; + + /** + * Information about the configuration data. Stores the type, + * default value and a documentation string for each configuration + * value. + * + * @var array layer => array(infotype => value, ...) + */ + var $configuration_info = array( + // Channels/Internet Access + 'default_channel' => array( + 'type' => 'string', + 'default' => PEAR_CONFIG_DEFAULT_CHANNEL, + 'doc' => 'the default channel to use for all non explicit commands', + 'prompt' => 'Default Channel', + 'group' => 'Internet Access', + ), + 'preferred_mirror' => array( + 'type' => 'string', + 'default' => PEAR_CONFIG_DEFAULT_CHANNEL, + 'doc' => 'the default server or mirror to use for channel actions', + 'prompt' => 'Default Channel Mirror', + 'group' => 'Internet Access', + ), + 'remote_config' => array( + 'type' => 'password', + 'default' => '', + 'doc' => 'ftp url of remote configuration file to use for synchronized install', + 'prompt' => 'Remote Configuration File', + 'group' => 'Internet Access', + ), + 'auto_discover' => array( + 'type' => 'integer', + 'default' => 0, + 'doc' => 'whether to automatically discover new channels', + 'prompt' => 'Auto-discover new Channels', + 'group' => 'Internet Access', + ), + // Internet Access + 'master_server' => array( + 'type' => 'string', + 'default' => 'pear.php.net', + 'doc' => 'name of the main PEAR server [NOT USED IN THIS VERSION]', + 'prompt' => 'PEAR server [DEPRECATED]', + 'group' => 'Internet Access', + ), + 'http_proxy' => array( + 'type' => 'string', + 'default' => PEAR_CONFIG_DEFAULT_HTTP_PROXY, + 'doc' => 'HTTP proxy (host:port) to use when downloading packages', + 'prompt' => 'HTTP Proxy Server Address', + 'group' => 'Internet Access', + ), + // File Locations + 'php_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_PHP_DIR, + 'doc' => 'directory where .php files are installed', + 'prompt' => 'PEAR directory', + 'group' => 'File Locations', + ), + 'ext_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_EXT_DIR, + 'doc' => 'directory where loadable extensions are installed', + 'prompt' => 'PHP extension directory', + 'group' => 'File Locations', + ), + 'doc_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_DOC_DIR, + 'doc' => 'directory where documentation is installed', + 'prompt' => 'PEAR documentation directory', + 'group' => 'File Locations', + ), + 'bin_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_BIN_DIR, + 'doc' => 'directory where executables are installed', + 'prompt' => 'PEAR executables directory', + 'group' => 'File Locations', + ), + 'data_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_DATA_DIR, + 'doc' => 'directory where data files are installed', + 'prompt' => 'PEAR data directory', + 'group' => 'File Locations (Advanced)', + ), + 'test_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_TEST_DIR, + 'doc' => 'directory where regression tests are installed', + 'prompt' => 'PEAR test directory', + 'group' => 'File Locations (Advanced)', + ), + 'cache_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_CACHE_DIR, + 'doc' => 'directory which is used for XMLRPC cache', + 'prompt' => 'PEAR Installer cache directory', + 'group' => 'File Locations (Advanced)', + ), + 'temp_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_TEMP_DIR, + 'doc' => 'directory which is used for all temp files', + 'prompt' => 'PEAR Installer temp directory', + 'group' => 'File Locations (Advanced)', + ), + 'download_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_CACHE_DIR, + 'doc' => 'directory which is used for all downloaded files', + 'prompt' => 'PEAR Installer download directory', + 'group' => 'File Locations (Advanced)', + ), + 'php_bin' => array( + 'type' => 'file', + 'default' => PEAR_CONFIG_DEFAULT_PHP_BIN, + 'doc' => 'PHP CLI/CGI binary for executing scripts', + 'prompt' => 'PHP CLI/CGI binary', + 'group' => 'File Locations (Advanced)', + ), + 'php_ini' => array( + 'type' => 'file', + 'default' => '', + 'doc' => 'location of php.ini in which to enable PECL extensions on install', + 'prompt' => 'php.ini location', + 'group' => 'File Locations (Advanced)', + ), + // Maintainers + 'username' => array( + 'type' => 'string', + 'default' => '', + 'doc' => '(maintainers) your PEAR account name', + 'prompt' => 'PEAR username (for maintainers)', + 'group' => 'Maintainers', + ), + 'password' => array( + 'type' => 'password', + 'default' => '', + 'doc' => '(maintainers) your PEAR account password', + 'prompt' => 'PEAR password (for maintainers)', + 'group' => 'Maintainers', + ), + // Advanced + 'verbose' => array( + 'type' => 'integer', + 'default' => PEAR_CONFIG_DEFAULT_VERBOSE, + 'doc' => 'verbosity level +0: really quiet +1: somewhat quiet +2: verbose +3: debug', + 'prompt' => 'Debug Log Level', + 'group' => 'Advanced', + ), + 'preferred_state' => array( + 'type' => 'set', + 'default' => PEAR_CONFIG_DEFAULT_PREFERRED_STATE, + 'doc' => 'the installer will prefer releases with this state when installing packages without a version or state specified', + 'valid_set' => array( + 'stable', 'beta', 'alpha', 'devel', 'snapshot'), + 'prompt' => 'Preferred Package State', + 'group' => 'Advanced', + ), + 'umask' => array( + 'type' => 'mask', + 'default' => PEAR_CONFIG_DEFAULT_UMASK, + 'doc' => 'umask used when creating files (Unix-like systems only)', + 'prompt' => 'Unix file mask', + 'group' => 'Advanced', + ), + 'cache_ttl' => array( + 'type' => 'integer', + 'default' => PEAR_CONFIG_DEFAULT_CACHE_TTL, + 'doc' => 'amount of secs where the local cache is used and not updated', + 'prompt' => 'Cache TimeToLive', + 'group' => 'Advanced', + ), + 'sig_type' => array( + 'type' => 'set', + 'default' => PEAR_CONFIG_DEFAULT_SIG_TYPE, + 'doc' => 'which package signature mechanism to use', + 'valid_set' => array('gpg'), + 'prompt' => 'Package Signature Type', + 'group' => 'Maintainers', + ), + 'sig_bin' => array( + 'type' => 'string', + 'default' => PEAR_CONFIG_DEFAULT_SIG_BIN, + 'doc' => 'which package signature mechanism to use', + 'prompt' => 'Signature Handling Program', + 'group' => 'Maintainers', + ), + 'sig_keyid' => array( + 'type' => 'string', + 'default' => '', + 'doc' => 'which key to use for signing with', + 'prompt' => 'Signature Key Id', + 'group' => 'Maintainers', + ), + 'sig_keydir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_SIG_KEYDIR, + 'doc' => 'directory where signature keys are located', + 'prompt' => 'Signature Key Directory', + 'group' => 'Maintainers', + ), + // __channels is reserved - used for channel-specific configuration + ); + + // }}} + + // {{{ PEAR_Config([file], [defaults_file]) + + /** + * Constructor. + * + * @param string file to read user-defined options from + * @param string file to read system-wide defaults from + * @param bool determines whether a registry object "follows" + * the value of php_dir (is automatically created + * and moved when php_dir is changed) + * @param bool if true, fails if configuration files cannot be loaded + * + * @access public + * + * @see PEAR_Config::singleton + */ + function PEAR_Config($user_file = '', $system_file = '', $ftp_file = false, + $strict = true) + { + $this->PEAR(); + PEAR_Installer_Role::initializeConfig($this); + $sl = DIRECTORY_SEPARATOR; + if (empty($user_file)) { + if (OS_WINDOWS) { + $user_file = PEAR_CONFIG_SYSCONFDIR . $sl . 'pear.ini'; + } else { + $user_file = getenv('HOME') . $sl . '.pearrc'; + } + } + if (empty($system_file)) { + if (OS_WINDOWS) { + $system_file = PEAR_CONFIG_SYSCONFDIR . $sl . 'pearsys.ini'; + } else { + $system_file = PEAR_CONFIG_SYSCONFDIR . $sl . 'pear.conf'; + } + } + + $this->layers = array_keys($this->configuration); + $this->files['user'] = $user_file; + $this->files['system'] = $system_file; + if ($user_file && file_exists($user_file)) { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $this->readConfigFile($user_file, 'user', $strict); + $this->popErrorHandling(); + if ($this->_errorsFound > 0) { + return; + } + } + + if ($system_file && file_exists($system_file)) { + $this->mergeConfigFile($system_file, false, 'system', $strict); + if ($this->_errorsFound > 0) { + return; + } + + } + + if (!$ftp_file) { + $ftp_file = $this->get('remote_config'); + } + + if ($ftp_file && defined('PEAR_REMOTEINSTALL_OK')) { + $this->readFTPConfigFile($ftp_file); + } + + foreach ($this->configuration_info as $key => $info) { + $this->configuration['default'][$key] = $info['default']; + } + + $this->_registry['default'] = &new PEAR_Registry($this->configuration['default']['php_dir']); + $this->_registry['default']->setConfig($this); + $this->_regInitialized['default'] = false; + //$GLOBALS['_PEAR_Config_instance'] = &$this; + } + + // }}} + // {{{ singleton([file], [defaults_file]) + + /** + * Static singleton method. If you want to keep only one instance + * of this class in use, this method will give you a reference to + * the last created PEAR_Config object if one exists, or create a + * new object. + * + * @param string (optional) file to read user-defined options from + * @param string (optional) file to read system-wide defaults from + * + * @return object an existing or new PEAR_Config instance + * + * @access public + * + * @see PEAR_Config::PEAR_Config + */ + function &singleton($user_file = '', $system_file = '', $strict = true) + { + if (is_object($GLOBALS['_PEAR_Config_instance'])) { + return $GLOBALS['_PEAR_Config_instance']; + } + + $t_conf = &new PEAR_Config($user_file, $system_file, false, $strict); + if ($t_conf->_errorsFound > 0) { + return $t_conf->lastError; + } + + $GLOBALS['_PEAR_Config_instance'] = &$t_conf; + return $GLOBALS['_PEAR_Config_instance']; + } + + // }}} + // {{{ validConfiguration() + + /** + * Determine whether any configuration files have been detected, and whether a + * registry object can be retrieved from this configuration. + * @return bool + * @since PEAR 1.4.0a1 + */ + function validConfiguration() + { + if ($this->isDefinedLayer('user') || $this->isDefinedLayer('system')) { + return true; + } + return false; + } + + // }}} + // {{{ readConfigFile([file], [layer]) + + /** + * Reads configuration data from a file. All existing values in + * the config layer are discarded and replaced with data from the + * file. + * @param string file to read from, if NULL or not specified, the + * last-used file for the same layer (second param) is used + * @param string config layer to insert data into ('user' or 'system') + * @return bool TRUE on success or a PEAR error on failure + */ + function readConfigFile($file = null, $layer = 'user', $strict = true) + { + if (empty($this->files[$layer])) { + return $this->raiseError("unknown config layer `$layer'"); + } + + if ($file === null) { + $file = $this->files[$layer]; + } + + $data = $this->_readConfigDataFrom($file); + + if (PEAR::isError($data)) { + if ($strict) { + $this->_errorsFound++; + $this->lastError = $data; + + return $data; + } else { + return true; + } + } else { + $this->files[$layer] = $file; + } + + $this->_decodeInput($data); + $this->configuration[$layer] = $data; + $this->_setupChannels(); + if (!$this->_noRegistry && ($phpdir = $this->get('php_dir', $layer, 'pear.php.net'))) { + $this->_registry[$layer] = &new PEAR_Registry($phpdir); + $this->_registry[$layer]->setConfig($this); + $this->_regInitialized[$layer] = false; + } else { + unset($this->_registry[$layer]); + } + return true; + } + + // }}} + + /** + * @param string url to the remote config file, like ftp://www.example.com/pear/config.ini + * @return true|PEAR_Error + */ + function readFTPConfigFile($path) + { + do { // poor man's try + if (!class_exists('PEAR_FTP')) { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (PEAR_Common::isIncludeable('PEAR/FTP.php')) { + require_once 'PEAR/FTP.php'; + } + } + if (class_exists('PEAR_FTP')) { + $this->_ftp = &new PEAR_FTP; + $this->_ftp->pushErrorHandling(PEAR_ERROR_RETURN); + $e = $this->_ftp->init($path); + if (PEAR::isError($e)) { + $this->_ftp->popErrorHandling(); + return $e; + } + $tmp = System::mktemp('-d'); + PEAR_Common::addTempFile($tmp); + $e = $this->_ftp->get(basename($path), $tmp . DIRECTORY_SEPARATOR . + 'pear.ini', false, FTP_BINARY); + if (PEAR::isError($e)) { + $this->_ftp->popErrorHandling(); + return $e; + } + PEAR_Common::addTempFile($tmp . DIRECTORY_SEPARATOR . 'pear.ini'); + $this->_ftp->disconnect(); + $this->_ftp->popErrorHandling(); + $this->files['ftp'] = $tmp . DIRECTORY_SEPARATOR . 'pear.ini'; + $e = $this->readConfigFile(null, 'ftp'); + if (PEAR::isError($e)) { + return $e; + } + $fail = array(); + foreach ($this->configuration_info as $key => $val) { + if (in_array($this->getGroup($key), + array('File Locations', 'File Locations (Advanced)')) && + $this->getType($key) == 'directory') { + // any directory configs must be set for this to work + if (!isset($this->configuration['ftp'][$key])) { + $fail[] = $key; + } + } + } + if (count($fail)) { + $fail = '"' . implode('", "', $fail) . '"'; + unset($this->files['ftp']); + unset($this->configuration['ftp']); + return PEAR::raiseError('ERROR: Ftp configuration file must set all ' . + 'directory configuration variables. These variables were not set: ' . + $fail); + } else { + return true; + } + } else { + return PEAR::raiseError('PEAR_RemoteInstaller must be installed to use remote config'); + } + } while (false); // poor man's catch + unset($this->files['ftp']); + return PEAR::raiseError('no remote host specified'); + } + + // {{{ _setupChannels() + + /** + * Reads the existing configurations and creates the _channels array from it + */ + function _setupChannels() + { + $set = array_flip(array_values($this->_channels)); + foreach ($this->configuration as $layer => $data) { + $i = 1000; + if (isset($data['__channels']) && is_array($data['__channels'])) { + foreach ($data['__channels'] as $channel => $info) { + $set[$channel] = $i++; + } + } + } + $this->_channels = array_values(array_flip($set)); + $this->setChannels($this->_channels); + } + + // }}} + // {{{ deleteChannel(channel) + + function deleteChannel($channel) + { + foreach ($this->configuration as $layer => $data) { + if (isset($data['__channels'])) { + if (isset($data['__channels'][strtolower($channel)])) { + unset($this->configuration[$layer]['__channels'][strtolower($channel)]); + } + } + } + $this->_channels = array_flip($this->_channels); + unset($this->_channels[strtolower($channel)]); + $this->_channels = array_flip($this->_channels); + } + + // }}} + // {{{ mergeConfigFile(file, [override], [layer]) + + /** + * Merges data into a config layer from a file. Does the same + * thing as readConfigFile, except it does not replace all + * existing values in the config layer. + * @param string file to read from + * @param bool whether to overwrite existing data (default TRUE) + * @param string config layer to insert data into ('user' or 'system') + * @param string if true, errors are returned if file opening fails + * @return bool TRUE on success or a PEAR error on failure + */ + function mergeConfigFile($file, $override = true, $layer = 'user', $strict = true) + { + if (empty($this->files[$layer])) { + return $this->raiseError("unknown config layer `$layer'"); + } + if ($file === null) { + $file = $this->files[$layer]; + } + $data = $this->_readConfigDataFrom($file); + if (PEAR::isError($data)) { + if ($strict) { + $this->_errorsFound++; + $this->lastError = $data; + + return $data; + } else { + return true; + } + } + $this->_decodeInput($data); + if ($override) { + $this->configuration[$layer] = + PEAR_Config::arrayMergeRecursive($this->configuration[$layer], $data); + } else { + $this->configuration[$layer] = + PEAR_Config::arrayMergeRecursive($data, $this->configuration[$layer]); + } + $this->_setupChannels(); + if (!$this->_noRegistry && ($phpdir = $this->get('php_dir', $layer, 'pear.php.net'))) { + $this->_registry[$layer] = &new PEAR_Registry($phpdir); + $this->_registry[$layer]->setConfig($this); + $this->_regInitialized[$layer] = false; + } else { + unset($this->_registry[$layer]); + } + return true; + } + + // }}} + // {{{ arrayMergeRecursive($arr2, $arr1) + /** + * @param array + * @param array + * @return array + * @static + */ + function arrayMergeRecursive($arr2, $arr1) + { + $ret = array(); + foreach ($arr2 as $key => $data) { + if (!isset($arr1[$key])) { + $ret[$key] = $data; + unset($arr1[$key]); + continue; + } + if (is_array($data)) { + if (!is_array($arr1[$key])) { + $ret[$key] = $arr1[$key]; + unset($arr1[$key]); + continue; + } + $ret[$key] = PEAR_Config::arrayMergeRecursive($arr1[$key], $arr2[$key]); + unset($arr1[$key]); + } + } + return array_merge($ret, $arr1); + } + + // }}} + // {{{ writeConfigFile([file], [layer]) + + /** + * Writes data into a config layer from a file. + * + * @param string|null file to read from, or null for default + * @param string config layer to insert data into ('user' or + * 'system') + * @param string|null data to write to config file or null for internal data [DEPRECATED] + * @return bool TRUE on success or a PEAR error on failure + */ + function writeConfigFile($file = null, $layer = 'user', $data = null) + { + $this->_lazyChannelSetup($layer); + if ($layer == 'both' || $layer == 'all') { + foreach ($this->files as $type => $file) { + $err = $this->writeConfigFile($file, $type, $data); + if (PEAR::isError($err)) { + return $err; + } + } + return true; + } + if (empty($this->files[$layer])) { + return $this->raiseError("unknown config file type `$layer'"); + } + if ($file === null) { + $file = $this->files[$layer]; + } + $data = ($data === null) ? $this->configuration[$layer] : $data; + $this->_encodeOutput($data); + $opt = array('-p', dirname($file)); + if (!@System::mkDir($opt)) { + return $this->raiseError("could not create directory: " . dirname($file)); + } + if (file_exists($file) && is_file($file) && !is_writeable($file)) { + return $this->raiseError("no write access to $file!"); + } + $fp = @fopen($file, "w"); + if (!$fp) { + return $this->raiseError("PEAR_Config::writeConfigFile fopen('$file','w') failed ($php_errormsg)"); + } + $contents = "#PEAR_Config 0.9\n" . serialize($data); + if (!@fwrite($fp, $contents)) { + return $this->raiseError("PEAR_Config::writeConfigFile: fwrite failed ($php_errormsg)"); + } + return true; + } + + // }}} + // {{{ _readConfigDataFrom(file) + + /** + * Reads configuration data from a file and returns the parsed data + * in an array. + * + * @param string file to read from + * + * @return array configuration data or a PEAR error on failure + * + * @access private + */ + function _readConfigDataFrom($file) + { + $fp = false; + if (file_exists($file)) { + $fp = @fopen($file, "r"); + } + if (!$fp) { + return $this->raiseError("PEAR_Config::readConfigFile fopen('$file','r') failed"); + } + $size = filesize($file); + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + fclose($fp); + $contents = file_get_contents($file); + if (empty($contents)) { + return $this->raiseError('Configuration file "' . $file . '" is empty'); + } + + set_magic_quotes_runtime($rt); + + $version = false; + if (preg_match('/^#PEAR_Config\s+(\S+)\s+/si', $contents, $matches)) { + $version = $matches[1]; + $contents = substr($contents, strlen($matches[0])); + } else { + // Museum config file + if (substr($contents,0,2) == 'a:') { + $version = '0.1'; + } + } + if ($version && version_compare("$version", '1', '<')) { + + // no '@', it is possible that unserialize + // raises a notice but it seems to block IO to + // STDOUT if a '@' is used and a notice is raise + $data = unserialize($contents); + + if (!is_array($data) && !$data) { + if ($contents == serialize(false)) { + $data = array(); + } else { + $err = $this->raiseError("PEAR_Config: bad data in $file"); + return $err; + } + } + if (!is_array($data)) { + if (strlen(trim($contents)) > 0) { + $error = "PEAR_Config: bad data in $file"; + $err = $this->raiseError($error); + return $err; + } else { + $data = array(); + } + } + // add parsing of newer formats here... + } else { + $err = $this->raiseError("$file: unknown version `$version'"); + return $err; + } + return $data; + } + + // }}} + // {{{ getConfFile(layer) + /** + * Gets the file used for storing the config for a layer + * + * @param string $layer 'user' or 'system' + */ + + function getConfFile($layer) + { + return $this->files[$layer]; + } + + // }}} + + /** + * @param array information on a role as parsed from its xml file + * @return true|PEAR_Error + * @access private + */ + function _addConfigVars($vars) + { + if (count($vars) > 3) { + return $this->raiseError('Roles can only define 3 new config variables or less'); + } + foreach ($vars as $name => $var) { + if (!is_array($var)) { + return $this->raiseError('Configuration information must be an array'); + } + if (!isset($var['type'])) { + return $this->raiseError('Configuration information must contain a type'); + } else { + if (!in_array($var['type'], + array('string', 'mask', 'password', 'directory', 'file', 'set'))) { + return $this->raiseError( + 'Configuration type must be one of directory, file, string, ' . + 'mask, set, or password'); + } + } + if (!isset($var['default'])) { + return $this->raiseError( + 'Configuration information must contain a default value ("default" index)'); + } else { + if (is_array($var['default'])) { + $real_default = ''; + foreach ($var['default'] as $config_var => $val) { + if (strpos($config_var, 'text') === 0) { + $real_default .= $val; + } elseif (strpos($config_var, 'constant') === 0) { + if (defined($val)) { + $real_default .= constant($val); + } else { + return $this->raiseError( + 'Unknown constant "' . $val . '" requested in ' . + 'default value for configuration variable "' . + $name . '"'); + } + } elseif (isset($this->configuration_info[$config_var])) { + $real_default .= + $this->configuration_info[$config_var]['default']; + } else { + return $this->raiseError( + 'Unknown request for "' . $config_var . '" value in ' . + 'default value for configuration variable "' . + $name . '"'); + } + } + $var['default'] = $real_default; + } + if ($var['type'] == 'integer') { + $var['default'] = (integer) $var['default']; + } + } + if (!isset($var['doc'])) { + return $this->raiseError( + 'Configuration information must contain a summary ("doc" index)'); + } + if (!isset($var['prompt'])) { + return $this->raiseError( + 'Configuration information must contain a simple prompt ("prompt" index)'); + } + if (!isset($var['group'])) { + return $this->raiseError( + 'Configuration information must contain a simple group ("group" index)'); + } + if (isset($this->configuration_info[$name])) { + return $this->raiseError('Configuration variable "' . $name . + '" already exists'); + } + $this->configuration_info[$name] = $var; + // fix bug #7351: setting custom config variable in a channel fails + $this->_channelConfigInfo[] = $name; + } + return true; + } + + // {{{ _encodeOutput(&data) + + /** + * Encodes/scrambles configuration data before writing to files. + * Currently, 'password' values will be base64-encoded as to avoid + * that people spot cleartext passwords by accident. + * + * @param array (reference) array to encode values in + * + * @return bool TRUE on success + * + * @access private + */ + function _encodeOutput(&$data) + { + foreach ($data as $key => $value) { + if ($key == '__channels') { + foreach ($data['__channels'] as $channel => $blah) { + $this->_encodeOutput($data['__channels'][$channel]); + } + } + if (!isset($this->configuration_info[$key])) { + continue; + } + $type = $this->configuration_info[$key]['type']; + switch ($type) { + // we base64-encode passwords so they are at least + // not shown in plain by accident + case 'password': { + $data[$key] = base64_encode($data[$key]); + break; + } + case 'mask': { + $data[$key] = octdec($data[$key]); + break; + } + } + } + return true; + } + + // }}} + // {{{ _decodeInput(&data) + + /** + * Decodes/unscrambles configuration data after reading from files. + * + * @param array (reference) array to encode values in + * + * @return bool TRUE on success + * + * @access private + * + * @see PEAR_Config::_encodeOutput + */ + function _decodeInput(&$data) + { + if (!is_array($data)) { + return true; + } + foreach ($data as $key => $value) { + if ($key == '__channels') { + foreach ($data['__channels'] as $channel => $blah) { + $this->_decodeInput($data['__channels'][$channel]); + } + } + if (!isset($this->configuration_info[$key])) { + continue; + } + $type = $this->configuration_info[$key]['type']; + switch ($type) { + case 'password': { + $data[$key] = base64_decode($data[$key]); + break; + } + case 'mask': { + $data[$key] = decoct($data[$key]); + break; + } + } + } + return true; + } + + // }}} + // {{{ getDefaultChannel([layer]) + /** + * Retrieve the default channel. + * + * On startup, channels are not initialized, so if the default channel is not + * pear.php.net, then initialize the config. + * @param string registry layer + * @return string|false + */ + function getDefaultChannel($layer = null) + { + $ret = false; + if ($layer === null) { + foreach ($this->layers as $layer) { + if (isset($this->configuration[$layer]['default_channel'])) { + $ret = $this->configuration[$layer]['default_channel']; + break; + } + } + } elseif (isset($this->configuration[$layer]['default_channel'])) { + $ret = $this->configuration[$layer]['default_channel']; + } + if ($ret == 'pear.php.net' && defined('PEAR_RUNTYPE') && PEAR_RUNTYPE == 'pecl') { + $ret = 'pecl.php.net'; + } + if ($ret) { + if ($ret != 'pear.php.net') { + $this->_lazyChannelSetup(); + } + return $ret; + } + return PEAR_CONFIG_DEFAULT_CHANNEL; + } + + // {{{ get(key, [layer]) + /** + * Returns a configuration value, prioritizing layers as per the + * layers property. + * + * @param string config key + * + * @return mixed the config value, or NULL if not found + * + * @access public + */ + function get($key, $layer = null, $channel = false) + { + if (!isset($this->configuration_info[$key])) { + return null; + } + if ($key == '__channels') { + return null; + } + if ($key == 'default_channel') { + return $this->getDefaultChannel($layer); + } + if (!$channel) { + $channel = $this->getDefaultChannel(); + } elseif ($channel != 'pear.php.net') { + $this->_lazyChannelSetup(); + } + $channel = strtolower($channel); + + $test = (in_array($key, $this->_channelConfigInfo)) ? + $this->_getChannelValue($key, $layer, $channel) : + null; + if ($test !== null) { + if ($this->_installRoot) { + if (in_array($this->getGroup($key), + array('File Locations', 'File Locations (Advanced)')) && + $this->getType($key) == 'directory') { + return $this->_prependPath($test, $this->_installRoot); + } + } + return $test; + } + if ($layer === null) { + foreach ($this->layers as $layer) { + if (isset($this->configuration[$layer][$key])) { + $test = $this->configuration[$layer][$key]; + if ($this->_installRoot) { + if (in_array($this->getGroup($key), + array('File Locations', 'File Locations (Advanced)')) && + $this->getType($key) == 'directory') { + return $this->_prependPath($test, $this->_installRoot); + } + } + if ($key == 'preferred_mirror') { + $reg = &$this->getRegistry(); + if (is_object($reg)) { + $chan = &$reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $channel; + } + if (!$chan->getMirror($test) && $chan->getName() != $test) { + return $channel; // mirror does not exist + } + } + } + return $test; + } + } + } elseif (isset($this->configuration[$layer][$key])) { + $test = $this->configuration[$layer][$key]; + if ($this->_installRoot) { + if (in_array($this->getGroup($key), + array('File Locations', 'File Locations (Advanced)')) && + $this->getType($key) == 'directory') { + return $this->_prependPath($test, $this->_installRoot); + } + } + if ($key == 'preferred_mirror') { + $reg = &$this->getRegistry(); + if (is_object($reg)) { + $chan = &$reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $channel; + } + if (!$chan->getMirror($test) && $chan->getName() != $test) { + return $channel; // mirror does not exist + } + } + } + return $test; + } + return null; + } + + // }}} + // {{{ _getChannelValue(key, value, [layer]) + /** + * Returns a channel-specific configuration value, prioritizing layers as per the + * layers property. + * + * @param string config key + * + * @return mixed the config value, or NULL if not found + * + * @access private + */ + function _getChannelValue($key, $layer, $channel) + { + if ($key == '__channels' || $channel == 'pear.php.net') { + return null; + } + $ret = null; + if ($layer === null) { + foreach ($this->layers as $ilayer) { + if (isset($this->configuration[$ilayer]['__channels'][$channel][$key])) { + $ret = $this->configuration[$ilayer]['__channels'][$channel][$key]; + break; + } + } + } elseif (isset($this->configuration[$layer]['__channels'][$channel][$key])) { + $ret = $this->configuration[$layer]['__channels'][$channel][$key]; + } + if ($key == 'preferred_mirror') { + if ($ret !== null) { + $reg = &$this->getRegistry($layer); + if (is_object($reg)) { + $chan = &$reg->getChannel($channel); + if (PEAR::isError($chan)) { + return $channel; + } + if (!$chan->getMirror($ret) && $chan->getName() != $ret) { + return $channel; // mirror does not exist + } + } + return $ret; + } + if ($channel != $this->getDefaultChannel($layer)) { + return $channel; // we must use the channel name as the preferred mirror + // if the user has not chosen an alternate + } else { + return $this->getDefaultChannel($layer); + } + } + return $ret; + } + + + // }}} + // {{{ set(key, value, [layer]) + + /** + * Set a config value in a specific layer (defaults to 'user'). + * Enforces the types defined in the configuration_info array. An + * integer config variable will be cast to int, and a set config + * variable will be validated against its legal values. + * + * @param string config key + * @param string config value + * @param string (optional) config layer + * @param string channel to set this value for, or null for global value + * @return bool TRUE on success, FALSE on failure + */ + function set($key, $value, $layer = 'user', $channel = false) + { + if ($key == '__channels') { + return false; + } + if (!isset($this->configuration[$layer])) { + return false; + } + if ($key == 'default_channel') { + // can only set this value globally + $channel = 'pear.php.net'; + if ($value != 'pear.php.net') { + $this->_lazyChannelSetup($layer); + } + } + if ($key == 'preferred_mirror') { + if ($channel == '__uri') { + return false; // can't set the __uri pseudo-channel's mirror + } + $reg = &$this->getRegistry($layer); + if (is_object($reg)) { + $chan = &$reg->getChannel($channel ? $channel : 'pear.php.net'); + if (PEAR::isError($chan)) { + return false; + } + if (!$chan->getMirror($value) && $chan->getName() != $value) { + return false; // mirror does not exist + } + } + } + if (empty($this->configuration_info[$key])) { + return false; + } + extract($this->configuration_info[$key]); + switch ($type) { + case 'integer': + $value = (int)$value; + break; + case 'set': { + // If a valid_set is specified, require the value to + // be in the set. If there is no valid_set, accept + // any value. + if ($valid_set) { + reset($valid_set); + if ((key($valid_set) === 0 && !in_array($value, $valid_set)) || + (key($valid_set) !== 0 && empty($valid_set[$value]))) + { + return false; + } + } + break; + } + } + if (!$channel) { + $channel = $this->get('default_channel', null, 'pear.php.net'); + } + if (!in_array($channel, $this->_channels)) { + $this->_lazyChannelSetup($layer); + $reg = &$this->getRegistry($layer); + if ($reg) { + $channel = $reg->channelName($channel); + } + if (!in_array($channel, $this->_channels)) { + return false; + } + } + if ($channel != 'pear.php.net') { + if (in_array($key, $this->_channelConfigInfo)) { + $this->configuration[$layer]['__channels'][$channel][$key] = $value; + return true; + } else { + return false; + } + } else { + if ($key == 'default_channel') { + if (!isset($reg)) { + $reg = &$this->getRegistry($layer); + if (!$reg) { + $reg = &$this->getRegistry(); + } + } + if ($reg) { + $value = $reg->channelName($value); + } + if (!$value) { + return false; + } + } + } + $this->configuration[$layer][$key] = $value; + if ($key == 'php_dir' && !$this->_noRegistry) { + if (!isset($this->_registry[$layer]) || + $value != $this->_registry[$layer]->install_dir) { + $this->_registry[$layer] = &new PEAR_Registry($value); + $this->_regInitialized[$layer] = false; + $this->_registry[$layer]->setConfig($this); + } + } + return true; + } + + // }}} + function _lazyChannelSetup($uselayer = false) + { + if ($this->_noRegistry) { + return; + } + $merge = false; + foreach ($this->_registry as $layer => $p) { + if ($uselayer && $uselayer != $layer) { + continue; + } + if (!$this->_regInitialized[$layer]) { + if ($layer == 'default' && isset($this->_registry['user']) || + isset($this->_registry['system'])) { + // only use the default registry if there are no alternatives + continue; + } + if (!is_object($this->_registry[$layer])) { + if ($phpdir = $this->get('php_dir', $layer, 'pear.php.net')) { + $this->_registry[$layer] = &new PEAR_Registry($phpdir); + $this->_registry[$layer]->setConfig($this); + $this->_regInitialized[$layer] = false; + } else { + unset($this->_registry[$layer]); + return; + } + } + $this->setChannels($this->_registry[$layer]->listChannels(), $merge); + $this->_regInitialized[$layer] = true; + $merge = true; + } + } + } + // {{{ setChannels() + + /** + * Set the list of channels. + * + * This should be set via a call to {@link PEAR_Registry::listChannels()} + * @param array + * @param bool + * @return bool success of operation + */ + function setChannels($channels, $merge = false) + { + if (!is_array($channels)) { + return false; + } + if ($merge) { + $this->_channels = array_merge($this->_channels, $channels); + } else { + $this->_channels = $channels; + } + foreach ($channels as $channel) { + $channel = strtolower($channel); + if ($channel == 'pear.php.net') { + continue; + } + foreach ($this->layers as $layer) { + if (!isset($this->configuration[$layer]['__channels'])) { + $this->configuration[$layer]['__channels'] = array(); + } + if (!isset($this->configuration[$layer]['__channels'][$channel]) + || !is_array($this->configuration[$layer]['__channels'][$channel])) { + $this->configuration[$layer]['__channels'][$channel] = array(); + } + } + } + return true; + } + + // }}} + // {{{ getType(key) + + /** + * Get the type of a config value. + * + * @param string config key + * + * @return string type, one of "string", "integer", "file", + * "directory", "set" or "password". + * + * @access public + * + */ + function getType($key) + { + if (isset($this->configuration_info[$key])) { + return $this->configuration_info[$key]['type']; + } + return false; + } + + // }}} + // {{{ getDocs(key) + + /** + * Get the documentation for a config value. + * + * @param string config key + * + * @return string documentation string + * + * @access public + * + */ + function getDocs($key) + { + if (isset($this->configuration_info[$key])) { + return $this->configuration_info[$key]['doc']; + } + return false; + } + // }}} + // {{{ getPrompt(key) + + /** + * Get the short documentation for a config value. + * + * @param string config key + * + * @return string short documentation string + * + * @access public + * + */ + function getPrompt($key) + { + if (isset($this->configuration_info[$key])) { + return $this->configuration_info[$key]['prompt']; + } + return false; + } + // }}} + // {{{ getGroup(key) + + /** + * Get the parameter group for a config key. + * + * @param string config key + * + * @return string parameter group + * + * @access public + * + */ + function getGroup($key) + { + if (isset($this->configuration_info[$key])) { + return $this->configuration_info[$key]['group']; + } + return false; + } + + // }}} + // {{{ getGroups() + + /** + * Get the list of parameter groups. + * + * @return array list of parameter groups + * + * @access public + * + */ + function getGroups() + { + $tmp = array(); + foreach ($this->configuration_info as $key => $info) { + $tmp[$info['group']] = 1; + } + return array_keys($tmp); + } + + // }}} + // {{{ getGroupKeys() + + /** + * Get the list of the parameters in a group. + * + * @param string $group parameter group + * + * @return array list of parameters in $group + * + * @access public + * + */ + function getGroupKeys($group) + { + $keys = array(); + foreach ($this->configuration_info as $key => $info) { + if ($info['group'] == $group) { + $keys[] = $key; + } + } + return $keys; + } + + // }}} + // {{{ getSetValues(key) + + /** + * Get the list of allowed set values for a config value. Returns + * NULL for config values that are not sets. + * + * @param string config key + * + * @return array enumerated array of set values, or NULL if the + * config key is unknown or not a set + * + * @access public + * + */ + function getSetValues($key) + { + if (isset($this->configuration_info[$key]) && + isset($this->configuration_info[$key]['type']) && + $this->configuration_info[$key]['type'] == 'set') + { + $valid_set = $this->configuration_info[$key]['valid_set']; + reset($valid_set); + if (key($valid_set) === 0) { + return $valid_set; + } + return array_keys($valid_set); + } + return null; + } + + // }}} + // {{{ getKeys() + + /** + * Get all the current config keys. + * + * @return array simple array of config keys + * + * @access public + */ + function getKeys() + { + $keys = array(); + foreach ($this->layers as $layer) { + $test = $this->configuration[$layer]; + if (isset($test['__channels'])) { + foreach ($test['__channels'] as $channel => $configs) { + $keys = array_merge($keys, $configs); + } + } + unset($test['__channels']); + $keys = array_merge($keys, $test); + } + return array_keys($keys); + } + + // }}} + // {{{ remove(key, [layer]) + + /** + * Remove the a config key from a specific config layer. + * + * @param string config key + * + * @param string (optional) config layer + * + * @return bool TRUE on success, FALSE on failure + * + * @access public + */ + function remove($key, $layer = 'user') + { + $channel = $this->getDefaultChannel(); + if ($channel !== 'pear.php.net') { + if (isset($this->configuration[$layer]['__channels'][$channel][$key])) { + unset($this->configuration[$layer]['__channels'][$channel][$key]); + return true; + } + } + if (isset($this->configuration[$layer][$key])) { + unset($this->configuration[$layer][$key]); + return true; + } + return false; + } + + // }}} + // {{{ removeLayer(layer) + + /** + * Temporarily remove an entire config layer. USE WITH CARE! + * + * @param string config key + * + * @param string (optional) config layer + * + * @return bool TRUE on success, FALSE on failure + * + * @access public + */ + function removeLayer($layer) + { + if (isset($this->configuration[$layer])) { + $this->configuration[$layer] = array(); + return true; + } + return false; + } + + // }}} + // {{{ store([layer]) + + /** + * Stores configuration data in a layer. + * + * @param string config layer to store + * + * @return bool TRUE on success, or PEAR error on failure + * + * @access public + */ + function store($layer = 'user', $data = null) + { + return $this->writeConfigFile(null, $layer, $data); + } + + // }}} + // {{{ toDefault(key) + + /** + * Unset the user-defined value of a config key, reverting the + * value to the system-defined one. + * + * @param string config key + * + * @return bool TRUE on success, FALSE on failure + * + * @access public + */ + function toDefault($key) + { + trigger_error("PEAR_Config::toDefault() deprecated, use PEAR_Config::remove() instead", E_USER_NOTICE); + return $this->remove($key, 'user'); + } + + // }}} + // {{{ definedBy(key) + + /** + * Tells what config layer that gets to define a key. + * + * @param string config key + * @param boolean return the defining channel + * + * @return string|array the config layer, or an empty string if not found. + * + * if $returnchannel, the return is an array array('layer' => layername, + * 'channel' => channelname), or an empty string if not found + * + * @access public + */ + function definedBy($key, $returnchannel = false) + { + foreach ($this->layers as $layer) { + $channel = $this->getDefaultChannel(); + if ($channel !== 'pear.php.net') { + if (isset($this->configuration[$layer]['__channels'][$channel][$key])) { + if ($returnchannel) { + return array('layer' => $layer, 'channel' => $channel); + } + return $layer; + } + } + if (isset($this->configuration[$layer][$key])) { + if ($returnchannel) { + return array('layer' => $layer, 'channel' => 'pear.php.net'); + } + return $layer; + } + } + return ''; + } + + // }}} + // {{{ isDefaulted(key) + + /** + * Tells whether a config value has a system-defined value. + * + * @param string config key + * + * @return bool + * + * @access public + * + * @deprecated + */ + function isDefaulted($key) + { + trigger_error("PEAR_Config::isDefaulted() deprecated, use PEAR_Config::definedBy() instead", E_USER_NOTICE); + return $this->definedBy($key) == 'system'; + } + + // }}} + // {{{ isDefined(key) + + /** + * Tells whether a given key exists as a config value. + * + * @param string config key + * + * @return bool whether exists in this object + * + * @access public + */ + function isDefined($key) + { + foreach ($this->layers as $layer) { + if (isset($this->configuration[$layer][$key])) { + return true; + } + } + return false; + } + + // }}} + // {{{ isDefinedLayer(key) + + /** + * Tells whether a given config layer exists. + * + * @param string config layer + * + * @return bool whether exists in this object + * + * @access public + */ + function isDefinedLayer($layer) + { + return isset($this->configuration[$layer]); + } + + // }}} + // {{{ getLayers() + + /** + * Returns the layers defined (except the 'default' one) + * + * @return array of the defined layers + */ + function getLayers() + { + $cf = $this->configuration; + unset($cf['default']); + return array_keys($cf); + } + + // }}} + // {{{ apiVersion() + function apiVersion() + { + return '1.1'; + } + // }}} + + /** + * @return PEAR_Registry + */ + function &getRegistry($use = null) + { + if ($use === null) { + $layer = 'user'; + } else { + $layer = $use; + } + if (isset($this->_registry[$layer])) { + return $this->_registry[$layer]; + } elseif ($use === null && isset($this->_registry['system'])) { + return $this->_registry['system']; + } elseif ($use === null && isset($this->_registry['default'])) { + return $this->_registry['default']; + } elseif ($use) { + $a = false; + return $a; + } else { + // only go here if null was passed in + echo "CRITICAL ERROR: Registry could not be initialized from any value"; + exit(1); + } + } + /** + * This is to allow customization like the use of installroot + * @param PEAR_Registry + * @return bool + */ + function setRegistry(&$reg, $layer = 'user') + { + if ($this->_noRegistry) { + return false; + } + if (!in_array($layer, array('user', 'system'))) { + return false; + } + $this->_registry[$layer] = &$reg; + if (is_object($reg)) { + $this->_registry[$layer]->setConfig($this); + } + return true; + } + + function noRegistry() + { + $this->_noRegistry = true; + } + + /** + * @return PEAR_Remote + */ + function &getRemote() + { + $remote = &new PEAR_Remote($this); + return $remote; + } + + /** + * @return PEAR_REST + */ + function &getREST($version, $options = array()) + { + $version = str_replace('.', '', $version); + if (!class_exists($class = 'PEAR_REST_' . $version)) { + require_once 'PEAR/REST/' . $version . '.php'; + } + $remote = &new $class($this, $options); + return $remote; + } + + /** + * The ftp server is set in {@link readFTPConfigFile()}. It exists only if a + * remote configuration file has been specified + * @return PEAR_FTP|false + */ + function &getFTP() + { + if (isset($this->_ftp)) { + return $this->_ftp; + } else { + $a = false; + return $a; + } + } + + // {{{ _prependPath($path, $prepend) + + function _prependPath($path, $prepend) + { + if (strlen($prepend) > 0) { + if (OS_WINDOWS && preg_match('/^[a-z]:/i', $path)) { + if (preg_match('/^[a-z]:/i', $prepend)) { + $prepend = substr($prepend, 2); + } elseif ($prepend{0} != '\\') { + $prepend = "\\$prepend"; + } + $path = substr($path, 0, 2) . $prepend . substr($path, 2); + } else { + $path = $prepend . $path; + } + } + return $path; + } + // }}} + + /** + * @param string|false installation directory to prepend to all _dir variables, or false to + * disable + */ + function setInstallRoot($root) + { + if (substr($root, -1) == DIRECTORY_SEPARATOR) { + $root = substr($root, 0, -1); + } + $old = $this->_installRoot; + $this->_installRoot = $root; + if (($old != $root) && !$this->_noRegistry) { + foreach (array_keys($this->_registry) as $layer) { + if ($layer == 'ftp' || !isset($this->_registry[$layer])) { + continue; + } + $this->_registry[$layer] = + &new PEAR_Registry($this->get('php_dir', $layer, 'pear.php.net')); + $this->_registry[$layer]->setConfig($this); + $this->_regInitialized[$layer] = false; + } + } + } +} + +?> diff --git a/PEAR/Dependency.php b/PEAR/Dependency.php new file mode 100644 index 0000000..372c320 --- /dev/null +++ b/PEAR/Dependency.php @@ -0,0 +1,495 @@ + | +// | Stig Bakken | +// +----------------------------------------------------------------------+ +// +// THIS FILE IS DEPRECATED IN FAVOR OF DEPENDENCY2.PHP, AND IS NOT USED IN THE INSTALLER +// $Id: Dependency.php,v 1.42 2006/03/26 23:25:56 cellog Exp $ + +require_once "PEAR.php"; +require_once "OS/Guess.php"; + +define('PEAR_DEPENDENCY_MISSING', -1); +define('PEAR_DEPENDENCY_CONFLICT', -2); +define('PEAR_DEPENDENCY_UPGRADE_MINOR', -3); +define('PEAR_DEPENDENCY_UPGRADE_MAJOR', -4); +define('PEAR_DEPENDENCY_BAD_DEPENDENCY', -5); +define('PEAR_DEPENDENCY_MISSING_OPTIONAL', -6); +define('PEAR_DEPENDENCY_CONFLICT_OPTIONAL', -7); +define('PEAR_DEPENDENCY_UPGRADE_MINOR_OPTIONAL', -8); +define('PEAR_DEPENDENCY_UPGRADE_MAJOR_OPTIONAL', -9); + +/** + * Dependency check for PEAR packages + * + * The class is based on the dependency RFC that can be found at + * http://cvs.php.net/cvs.php/pearweb/rfc. It requires PHP >= 4.1 + * + * @author Tomas V.V.Vox + * @author Stig Bakken + */ +class PEAR_Dependency +{ + // {{{ constructor + /** + * Constructor + * + * @access public + * @param object Registry object + * @return void + */ + function PEAR_Dependency(&$registry) + { + $this->registry = &$registry; + } + + // }}} + // {{{ callCheckMethod() + + /** + * This method maps the XML dependency definition to the + * corresponding one from PEAR_Dependency + * + *
+    * $opts => Array
+    *    (
+    *        [type] => pkg
+    *        [rel] => ge
+    *        [version] => 3.4
+    *        [name] => HTML_Common
+    *        [optional] => false
+    *    )
+    * 
+ * + * @param string Error message + * @param array Options + * @return boolean + */ + function callCheckMethod(&$errmsg, $opts) + { + $rel = isset($opts['rel']) ? $opts['rel'] : 'has'; + $req = isset($opts['version']) ? $opts['version'] : null; + $name = isset($opts['name']) ? $opts['name'] : null; + $channel = isset($opts['channel']) ? $opts['channel'] : 'pear.php.net'; + $opt = (isset($opts['optional']) && $opts['optional'] == 'yes') ? + $opts['optional'] : null; + $errmsg = ''; + switch ($opts['type']) { + case 'pkg': + return $this->checkPackage($errmsg, $name, $req, $rel, $opt, $channel); + break; + case 'ext': + return $this->checkExtension($errmsg, $name, $req, $rel, $opt); + break; + case 'php': + return $this->checkPHP($errmsg, $req, $rel); + break; + case 'prog': + return $this->checkProgram($errmsg, $name); + break; + case 'os': + return $this->checkOS($errmsg, $name); + break; + case 'sapi': + return $this->checkSAPI($errmsg, $name); + break; + case 'zend': + return $this->checkZend($errmsg, $name); + break; + default: + return "'{$opts['type']}' dependency type not supported"; + } + } + + // }}} + // {{{ checkPackage() + + /** + * Package dependencies check method + * + * @param string $errmsg Empty string, it will be populated with an error message, if any + * @param string $name Name of the package to test + * @param string $req The package version required + * @param string $relation How to compare versions with each other + * @param bool $opt Whether the relationship is optional + * @param string $channel Channel name + * + * @return mixed bool false if no error or the error string + */ + function checkPackage(&$errmsg, $name, $req = null, $relation = 'has', + $opt = false, $channel = 'pear.php.net') + { + if (is_string($req) && substr($req, 0, 2) == 'v.') { + $req = substr($req, 2); + } + switch ($relation) { + case 'has': + if (!$this->registry->packageExists($name, $channel)) { + if ($opt) { + $errmsg = "package `$channel/$name' is recommended to utilize some features."; + return PEAR_DEPENDENCY_MISSING_OPTIONAL; + } + $errmsg = "requires package `$channel/$name'"; + return PEAR_DEPENDENCY_MISSING; + } + return false; + case 'not': + if ($this->registry->packageExists($name, $channel)) { + $errmsg = "conflicts with package `$channel/$name'"; + return PEAR_DEPENDENCY_CONFLICT; + } + return false; + case 'lt': + case 'le': + case 'eq': + case 'ne': + case 'ge': + case 'gt': + $version = $this->registry->packageInfo($name, 'version', $channel); + if (!$this->registry->packageExists($name, $channel) + || !version_compare("$version", "$req", $relation)) + { + $code = $this->codeFromRelation($relation, $version, $req, $opt); + if ($opt) { + $errmsg = "package `$channel/$name' version " . $this->signOperator($relation) . + " $req is recommended to utilize some features."; + if ($version) { + $errmsg .= " Installed version is $version"; + } + return $code; + } + $errmsg = "requires package `$channel/$name' " . + $this->signOperator($relation) . " $req"; + return $code; + } + return false; + } + $errmsg = "relation '$relation' with requirement '$req' is not supported (name=$channel/$name)"; + return PEAR_DEPENDENCY_BAD_DEPENDENCY; + } + + // }}} + // {{{ checkPackageUninstall() + + /** + * Check package dependencies on uninstall + * + * @param string $error The resultant error string + * @param string $warning The resultant warning string + * @param string $name Name of the package to test + * @param string $channel Channel name of the package + * + * @return bool true if there were errors + */ + function checkPackageUninstall(&$error, &$warning, $package, $channel = 'pear.php.net') + { + $channel = strtolower($channel); + $error = null; + $channels = $this->registry->listAllPackages(); + foreach ($channels as $channelname => $packages) { + foreach ($packages as $pkg) { + if ($pkg == $package && $channel == $channelname) { + continue; + } + $deps = $this->registry->packageInfo($pkg, 'release_deps', $channel); + if (empty($deps)) { + continue; + } + foreach ($deps as $dep) { + $depchannel = isset($dep['channel']) ? $dep['channel'] : 'pear.php.net'; + if ($dep['type'] == 'pkg' && (strcasecmp($dep['name'], $package) == 0) && + ($depchannel == $channel)) { + if ($dep['rel'] == 'ne') { + continue; + } + if (isset($dep['optional']) && $dep['optional'] == 'yes') { + $warning .= "\nWarning: Package '$depchannel/$pkg' optionally depends on '$channel:/package'"; + } else { + $error .= "Package '$depchannel/$pkg' depends on '$channel/$package'\n"; + } + } + } + } + } + return ($error) ? true : false; + } + + // }}} + // {{{ checkExtension() + + /** + * Extension dependencies check method + * + * @param string $name Name of the extension to test + * @param string $req_ext_ver Required extension version to compare with + * @param string $relation How to compare versions with eachother + * @param bool $opt Whether the relationship is optional + * + * @return mixed bool false if no error or the error string + */ + function checkExtension(&$errmsg, $name, $req = null, $relation = 'has', + $opt = false) + { + if ($relation == 'not') { + if (extension_loaded($name)) { + $errmsg = "conflicts with PHP extension '$name'"; + return PEAR_DEPENDENCY_CONFLICT; + } else { + return false; + } + } + + if (!extension_loaded($name)) { + if ($relation == 'ne') { + return false; + } + if ($opt) { + $errmsg = "'$name' PHP extension is recommended to utilize some features"; + return PEAR_DEPENDENCY_MISSING_OPTIONAL; + } + $errmsg = "'$name' PHP extension is not installed"; + return PEAR_DEPENDENCY_MISSING; + } + if ($relation == 'has') { + return false; + } + $code = false; + if (is_string($req) && substr($req, 0, 2) == 'v.') { + $req = substr($req, 2); + } + $ext_ver = phpversion($name); + $operator = $relation; + // Force params to be strings, otherwise the comparation will fail (ex. 0.9==0.90) + if (!version_compare("$ext_ver", "$req", $operator)) { + $errmsg = "'$name' PHP extension version " . + $this->signOperator($operator) . " $req is required"; + $code = $this->codeFromRelation($relation, $ext_ver, $req, $opt); + if ($opt) { + $errmsg = "'$name' PHP extension version " . $this->signOperator($operator) . + " $req is recommended to utilize some features"; + return $code; + } + } + return $code; + } + + // }}} + // {{{ checkOS() + + /** + * Operating system dependencies check method + * + * @param string $os Name of the operating system + * + * @return mixed bool false if no error or the error string + */ + function checkOS(&$errmsg, $os) + { + // XXX Fixme: Implement a more flexible way, like + // comma separated values or something similar to PEAR_OS + static $myos; + if (empty($myos)) { + $myos = new OS_Guess(); + } + // only 'has' relation is currently supported + if ($myos->matchSignature($os)) { + return false; + } + $errmsg = "'$os' operating system not supported"; + return PEAR_DEPENDENCY_CONFLICT; + } + + // }}} + // {{{ checkPHP() + + /** + * PHP version check method + * + * @param string $req which version to compare + * @param string $relation how to compare the version + * + * @return mixed bool false if no error or the error string + */ + function checkPHP(&$errmsg, $req, $relation = 'ge') + { + // this would be a bit stupid, but oh well :) + if ($relation == 'has') { + return false; + } + if ($relation == 'not') { + $errmsg = "Invalid dependency - 'not' is allowed when specifying PHP, you must run PHP in PHP"; + return PEAR_DEPENDENCY_BAD_DEPENDENCY; + } + if (substr($req, 0, 2) == 'v.') { + $req = substr($req,2, strlen($req) - 2); + } + $php_ver = phpversion(); + $operator = $relation; + if (!version_compare("$php_ver", "$req", $operator)) { + $errmsg = "PHP version " . $this->signOperator($operator) . + " $req is required"; + return PEAR_DEPENDENCY_CONFLICT; + } + return false; + } + + // }}} + // {{{ checkProgram() + + /** + * External program check method. Looks for executable files in + * directories listed in the PATH environment variable. + * + * @param string $program which program to look for + * + * @return mixed bool false if no error or the error string + */ + function checkProgram(&$errmsg, $program) + { + // XXX FIXME honor safe mode + $exe_suffix = OS_WINDOWS ? '.exe' : ''; + $path_elements = explode(PATH_SEPARATOR, getenv('PATH')); + foreach ($path_elements as $dir) { + $file = $dir . DIRECTORY_SEPARATOR . $program . $exe_suffix; + if (file_exists($file) && is_executable($file)) { + return false; + } + } + $errmsg = "'$program' program is not present in the PATH"; + return PEAR_DEPENDENCY_MISSING; + } + + // }}} + // {{{ checkSAPI() + + /** + * SAPI backend check method. Version comparison is not yet + * available here. + * + * @param string $name name of SAPI backend + * @param string $req which version to compare + * @param string $relation how to compare versions (currently + * hardcoded to 'has') + * @return mixed bool false if no error or the error string + */ + function checkSAPI(&$errmsg, $name, $req = null, $relation = 'has') + { + // XXX Fixme: There is no way to know if the user has or + // not other SAPI backends installed than the installer one + + $sapi_backend = php_sapi_name(); + // Version comparisons not supported, sapi backends don't have + // version information yet. + if ($sapi_backend == $name) { + return false; + } + $errmsg = "'$sapi_backend' SAPI backend not supported"; + return PEAR_DEPENDENCY_CONFLICT; + } + + // }}} + // {{{ checkZend() + + /** + * Zend version check method + * + * @param string $req which version to compare + * @param string $relation how to compare the version + * + * @return mixed bool false if no error or the error string + */ + function checkZend(&$errmsg, $req, $relation = 'ge') + { + if (substr($req, 0, 2) == 'v.') { + $req = substr($req,2, strlen($req) - 2); + } + $zend_ver = zend_version(); + $operator = substr($relation,0,2); + if (!version_compare("$zend_ver", "$req", $operator)) { + $errmsg = "Zend version " . $this->signOperator($operator) . + " $req is required"; + return PEAR_DEPENDENCY_CONFLICT; + } + return false; + } + + // }}} + // {{{ signOperator() + + /** + * Converts text comparing operators to them sign equivalents + * + * Example: 'ge' to '>=' + * + * @access public + * @param string Operator + * @return string Sign equivalent + */ + function signOperator($operator) + { + switch($operator) { + case 'lt': return '<'; + case 'le': return '<='; + case 'gt': return '>'; + case 'ge': return '>='; + case 'eq': return '=='; + case 'ne': return '!='; + default: + return $operator; + } + } + + // }}} + // {{{ codeFromRelation() + + /** + * Convert relation into corresponding code + * + * @access public + * @param string Relation + * @param string Version + * @param string Requirement + * @param bool Optional dependency indicator + * @return integer + */ + function codeFromRelation($relation, $version, $req, $opt = false) + { + $code = PEAR_DEPENDENCY_BAD_DEPENDENCY; + switch ($relation) { + case 'gt': case 'ge': case 'eq': + // upgrade + $have_major = preg_replace('/\D.*/', '', $version); + $need_major = preg_replace('/\D.*/', '', $req); + if ($need_major > $have_major) { + $code = $opt ? PEAR_DEPENDENCY_UPGRADE_MAJOR_OPTIONAL : + PEAR_DEPENDENCY_UPGRADE_MAJOR; + } else { + $code = $opt ? PEAR_DEPENDENCY_UPGRADE_MINOR_OPTIONAL : + PEAR_DEPENDENCY_UPGRADE_MINOR; + } + break; + case 'lt': case 'le': case 'ne': + $code = $opt ? PEAR_DEPENDENCY_CONFLICT_OPTIONAL : + PEAR_DEPENDENCY_CONFLICT; + break; + } + return $code; + } + + // }}} +} +?> diff --git a/PEAR/Dependency2.php b/PEAR/Dependency2.php new file mode 100644 index 0000000..5f54735 --- /dev/null +++ b/PEAR/Dependency2.php @@ -0,0 +1,1299 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Dependency2.php,v 1.55 2007/03/21 06:10:46 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Required for the PEAR_VALIDATE_* constants + */ +require_once 'PEAR/Validate.php'; + +/** + * Dependency check for PEAR packages + * + * This class handles both version 1.0 and 2.0 dependencies + * WARNING: *any* changes to this class must be duplicated in the + * test_PEAR_Dependency2 class found in tests/PEAR_Dependency2/setup.php.inc, + * or unit tests will not actually validate the changes + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Dependency2 +{ + /** + * One of the PEAR_VALIDATE_* states + * @see PEAR_VALIDATE_NORMAL + * @var integer + */ + var $_state; + /** + * Command-line options to install/upgrade/uninstall commands + * @param array + */ + var $_options; + /** + * @var OS_Guess + */ + var $_os; + /** + * @var PEAR_Registry + */ + var $_registry; + /** + * @var PEAR_Config + */ + var $_config; + /** + * @var PEAR_DependencyDB + */ + var $_dependencydb; + /** + * Output of PEAR_Registry::parsedPackageName() + * @var array + */ + var $_currentPackage; + /** + * @param PEAR_Config + * @param array installation options + * @param array format of PEAR_Registry::parsedPackageName() + * @param int installation state (one of PEAR_VALIDATE_*) + */ + function PEAR_Dependency2(&$config, $installoptions, $package, + $state = PEAR_VALIDATE_INSTALLING) + { + $this->_config = &$config; + if (!class_exists('PEAR_DependencyDB')) { + require_once 'PEAR/DependencyDB.php'; + } + if (isset($installoptions['packagingroot'])) { + // make sure depdb is in the right location + $config->setInstallRoot($installoptions['packagingroot']); + } + $this->_registry = &$config->getRegistry(); + $this->_dependencydb = &PEAR_DependencyDB::singleton($config); + if (isset($installoptions['packagingroot'])) { + $config->setInstallRoot(false); + } + $this->_options = $installoptions; + $this->_state = $state; + if (!class_exists('OS_Guess')) { + require_once 'OS/Guess.php'; + } + $this->_os = new OS_Guess; + $this->_currentPackage = $package; + } + + function _getExtraString($dep) + { + $extra = ' ('; + if (isset($dep['uri'])) { + return ''; + } + if (isset($dep['recommended'])) { + $extra .= 'recommended version ' . $dep['recommended']; + } else { + if (isset($dep['min'])) { + $extra .= 'version >= ' . $dep['min']; + } + if (isset($dep['max'])) { + if ($extra != ' (') { + $extra .= ', '; + } + $extra .= 'version <= ' . $dep['max']; + } + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + if ($extra != ' (') { + $extra .= ', '; + } + $extra .= 'excluded versions: '; + foreach ($dep['exclude'] as $i => $exclude) { + if ($i) { + $extra .= ', '; + } + $extra .= $exclude; + } + } + } + $extra .= ')'; + if ($extra == ' ()') { + $extra = ''; + } + return $extra; + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function getPHP_OS() + { + return PHP_OS; + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function getsysname() + { + return $this->_os->getSysname(); + } + + /** + * Specify a dependency on an OS. Use arch for detailed os/processor information + * + * There are two generic OS dependencies that will be the most common, unix and windows. + * Other options are linux, freebsd, darwin (OS X), sunos, irix, hpux, aix + */ + function validateOsDependency($dep) + { + if ($this->_state != PEAR_VALIDATE_INSTALLING && + $this->_state != PEAR_VALIDATE_DOWNLOADING) { + return true; + } + if (isset($dep['conflicts'])) { + $not = true; + } else { + $not = false; + } + if ($dep['name'] == '*') { + return true; + } + switch (strtolower($dep['name'])) { + case 'windows' : + if ($not) { + if (strtolower(substr($this->getPHP_OS(), 0, 3)) == 'win') { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError("Cannot install %s on Windows"); + } else { + return $this->warning("warning: Cannot install %s on Windows"); + } + } + } else { + if (strtolower(substr($this->getPHP_OS(), 0, 3)) != 'win') { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError("Can only install %s on Windows"); + } else { + return $this->warning("warning: Can only install %s on Windows"); + } + } + } + break; + case 'unix' : + $unices = array('linux', 'freebsd', 'darwin', 'sunos', 'irix', 'hpux', 'aix'); + if ($not) { + if (in_array($this->getSysname(), $unices)) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError("Cannot install %s on any Unix system"); + } else { + return $this->warning( + "warning: Cannot install %s on any Unix system"); + } + } + } else { + if (!in_array($this->getSysname(), $unices)) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError("Can only install %s on a Unix system"); + } else { + return $this->warning( + "warning: Can only install %s on a Unix system"); + } + } + } + break; + default : + if ($not) { + if (strtolower($dep['name']) == strtolower($this->getSysname())) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError('Cannot install %s on ' . $dep['name'] . + ' operating system'); + } else { + return $this->warning('warning: Cannot install %s on ' . + $dep['name'] . ' operating system'); + } + } + } else { + if (strtolower($dep['name']) != strtolower($this->getSysname())) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError('Cannot install %s on ' . + $this->getSysname() . + ' operating system, can only install on ' . $dep['name']); + } else { + return $this->warning('warning: Cannot install %s on ' . + $this->getSysname() . + ' operating system, can only install on ' . $dep['name']); + } + } + } + } + return true; + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function matchSignature($pattern) + { + return $this->_os->matchSignature($pattern); + } + + /** + * Specify a complex dependency on an OS/processor/kernel version, + * Use OS for simple operating system dependency. + * + * This is the only dependency that accepts an eregable pattern. The pattern + * will be matched against the php_uname() output parsed by OS_Guess + */ + function validateArchDependency($dep) + { + if ($this->_state != PEAR_VALIDATE_INSTALLING) { + return true; + } + if (isset($dep['conflicts'])) { + $not = true; + } else { + $not = false; + } + if (!$this->matchSignature($dep['pattern'])) { + if (!$not) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s Architecture dependency failed, does not ' . + 'match "' . $dep['pattern'] . '"'); + } else { + return $this->warning('warning: %s Architecture dependency failed, does ' . + 'not match "' . $dep['pattern'] . '"'); + } + } + return true; + } else { + if ($not) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s Architecture dependency failed, required "' . + $dep['pattern'] . '"'); + } else { + return $this->warning('warning: %s Architecture dependency failed, ' . + 'required "' . $dep['pattern'] . '"'); + } + } + return true; + } + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function extension_loaded($name) + { + return extension_loaded($name); + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function phpversion($name = null) + { + if ($name !== null) { + return phpversion($name); + } else { + return phpversion(); + } + } + + function validateExtensionDependency($dep, $required = true) + { + if ($this->_state != PEAR_VALIDATE_INSTALLING && + $this->_state != PEAR_VALIDATE_DOWNLOADING) { + return true; + } + $loaded = $this->extension_loaded($dep['name']); + $extra = $this->_getExtraString($dep); + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + } + if (!isset($dep['min']) && !isset($dep['max']) && + !isset($dep['recommended']) && !isset($dep['exclude'])) { + if ($loaded) { + if (isset($dep['conflicts'])) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra); + } else { + return $this->warning('warning: %s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra); + } + } + return true; + } else { + if (isset($dep['conflicts'])) { + return true; + } + if ($required) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PHP extension "' . + $dep['name'] . '"' . $extra); + } else { + return $this->warning('warning: %s requires PHP extension "' . + $dep['name'] . '"' . $extra); + } + } else { + return $this->warning('%s can optionally use PHP extension "' . + $dep['name'] . '"' . $extra); + } + } + } + if (!$loaded) { + if (isset($dep['conflicts'])) { + return true; + } + if (!$required) { + return $this->warning('%s can optionally use PHP extension "' . + $dep['name'] . '"' . $extra); + } else { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PHP extension "' . $dep['name'] . + '"' . $extra); + } + return $this->warning('warning: %s requires PHP extension "' . $dep['name'] . + '"' . $extra); + } + } + $version = (string) $this->phpversion($dep['name']); + if (empty($version)) { + $version = '0'; + } + $fail = false; + if (isset($dep['min'])) { + if (!version_compare($version, $dep['min'], '>=')) { + $fail = true; + } + } + if (isset($dep['max'])) { + if (!version_compare($version, $dep['max'], '<=')) { + $fail = true; + } + } + if ($fail && !isset($dep['conflicts'])) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PHP extension "' . $dep['name'] . + '"' . $extra . ', installed version is ' . $version); + } else { + return $this->warning('warning: %s requires PHP extension "' . $dep['name'] . + '"' . $extra . ', installed version is ' . $version); + } + } elseif ((isset($dep['min']) || isset($dep['max'])) && !$fail && isset($dep['conflicts'])) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra . ', installed version is ' . $version); + } else { + return $this->warning('warning: %s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra . ', installed version is ' . $version); + } + } + if (isset($dep['exclude'])) { + foreach ($dep['exclude'] as $exclude) { + if (version_compare($version, $exclude, '==')) { + if (isset($dep['conflicts'])) { + continue; + } + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError('%s is not compatible with PHP extension "' . + $dep['name'] . '" version ' . + $exclude); + } else { + return $this->warning('warning: %s is not compatible with PHP extension "' . + $dep['name'] . '" version ' . + $exclude); + } + } elseif (version_compare($version, $exclude, '!=') && isset($dep['conflicts'])) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra . ', installed version is ' . $version); + } else { + return $this->warning('warning: %s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra . ', installed version is ' . $version); + } + } + } + } + if (isset($dep['recommended'])) { + if (version_compare($version, $dep['recommended'], '==')) { + return true; + } else { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s dependency: PHP extension ' . $dep['name'] . + ' version "' . $version . '"' . + ' is not the recommended version "' . $dep['recommended'] . + '", but may be compatible, use --force to install'); + } else { + return $this->warning('warning: %s dependency: PHP extension ' . + $dep['name'] . ' version "' . $version . '"' . + ' is not the recommended version "' . $dep['recommended'].'"'); + } + } + } + return true; + } + + function validatePhpDependency($dep) + { + if ($this->_state != PEAR_VALIDATE_INSTALLING && + $this->_state != PEAR_VALIDATE_DOWNLOADING) { + return true; + } + $version = $this->phpversion(); + $extra = $this->_getExtraString($dep); + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + } + if (isset($dep['min'])) { + if (!version_compare($version, $dep['min'], '>=')) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PHP' . + $extra . ', installed version is ' . $version); + } else { + return $this->warning('warning: %s requires PHP' . + $extra . ', installed version is ' . $version); + } + } + } + if (isset($dep['max'])) { + if (!version_compare($version, $dep['max'], '<=')) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PHP' . + $extra . ', installed version is ' . $version); + } else { + return $this->warning('warning: %s requires PHP' . + $extra . ', installed version is ' . $version); + } + } + } + if (isset($dep['exclude'])) { + foreach ($dep['exclude'] as $exclude) { + if (version_compare($version, $exclude, '==')) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError('%s is not compatible with PHP version ' . + $exclude); + } else { + return $this->warning( + 'warning: %s is not compatible with PHP version ' . + $exclude); + } + } + } + } + return true; + } + + /** + * This makes unit-testing a heck of a lot easier + */ + function getPEARVersion() + { + return '1.6.1'; + } + + function validatePearinstallerDependency($dep) + { + $pearversion = $this->getPEARVersion(); + $extra = $this->_getExtraString($dep); + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + } + if (version_compare($pearversion, $dep['min'], '<')) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PEAR Installer' . $extra . + ', installed version is ' . $pearversion); + } else { + return $this->warning('warning: %s requires PEAR Installer' . $extra . + ', installed version is ' . $pearversion); + } + } + if (isset($dep['max'])) { + if (version_compare($pearversion, $dep['max'], '>')) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires PEAR Installer' . $extra . + ', installed version is ' . $pearversion); + } else { + return $this->warning('warning: %s requires PEAR Installer' . $extra . + ', installed version is ' . $pearversion); + } + } + } + if (isset($dep['exclude'])) { + if (!isset($dep['exclude'][0])) { + $dep['exclude'] = array($dep['exclude']); + } + foreach ($dep['exclude'] as $exclude) { + if (version_compare($exclude, $pearversion, '==')) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s is not compatible with PEAR Installer ' . + 'version ' . $exclude); + } else { + return $this->warning('warning: %s is not compatible with PEAR ' . + 'Installer version ' . $exclude); + } + } + } + } + return true; + } + + function validateSubpackageDependency($dep, $required, $params) + { + return $this->validatePackageDependency($dep, $required, $params); + } + + /** + * @param array dependency information (2.0 format) + * @param boolean whether this is a required dependency + * @param array a list of downloaded packages to be installed, if any + * @param boolean if true, then deps on pear.php.net that fail will also check + * against pecl.php.net packages to accomodate extensions that have + * moved to pecl.php.net from pear.php.net + */ + function validatePackageDependency($dep, $required, $params, $depv1 = false) + { + if ($this->_state != PEAR_VALIDATE_INSTALLING && + $this->_state != PEAR_VALIDATE_DOWNLOADING) { + return true; + } + if (isset($dep['providesextension'])) { + if ($this->extension_loaded($dep['providesextension'])) { + $save = $dep; + $subdep = $dep; + $subdep['name'] = $subdep['providesextension']; + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $ret = $this->validateExtensionDependency($subdep, $required); + PEAR::popErrorHandling(); + if (!PEAR::isError($ret)) { + return true; + } + } + } + if ($this->_state == PEAR_VALIDATE_INSTALLING) { + return $this->_validatePackageInstall($dep, $required, $depv1); + } + if ($this->_state == PEAR_VALIDATE_DOWNLOADING) { + return $this->_validatePackageDownload($dep, $required, $params, $depv1); + } + } + + function _validatePackageDownload($dep, $required, $params, $depv1 = false) + { + $dep['package'] = $dep['name']; + if (isset($dep['uri'])) { + $dep['channel'] = '__uri'; + } + $depname = $this->_registry->parsedPackageNameToString($dep, true); + $found = false; + foreach ($params as $param) { + if ($param->isEqual( + array('package' => $dep['name'], + 'channel' => $dep['channel']))) { + $found = true; + break; + } + if ($depv1 && $dep['channel'] == 'pear.php.net') { + if ($param->isEqual( + array('package' => $dep['name'], + 'channel' => 'pecl.php.net'))) { + $found = true; + break; + } + } + } + if (!$found && isset($dep['providesextension'])) { + foreach ($params as $param) { + if ($param->isExtension($dep['providesextension'])) { + $found = true; + break; + } + } + } + if ($found) { + $version = $param->getVersion(); + $installed = false; + $downloaded = true; + } else { + if ($this->_registry->packageExists($dep['name'], $dep['channel'])) { + $installed = true; + $downloaded = false; + $version = $this->_registry->packageinfo($dep['name'], 'version', + $dep['channel']); + } else { + if ($dep['channel'] == 'pecl.php.net' && $this->_registry->packageExists($dep['name'], + 'pear.php.net')) { + $installed = true; + $downloaded = false; + $version = $this->_registry->packageinfo($dep['name'], 'version', + 'pear.php.net'); + } else { + $version = 'not installed or downloaded'; + $installed = false; + $downloaded = false; + } + } + } + $extra = $this->_getExtraString($dep); + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + } + if (!isset($dep['min']) && !isset($dep['max']) && + !isset($dep['recommended']) && !isset($dep['exclude'])) { + if ($installed || $downloaded) { + $installed = $installed ? 'installed' : 'downloaded'; + if (isset($dep['conflicts'])) { + if ($version) { + $rest = ", $installed version is " . $version; + } else { + $rest = ''; + } + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with package "' . $depname . '"' . + $extra . $rest); + } else { + return $this->warning('warning: %s conflicts with package "' . $depname . '"' . + $extra . $rest); + } + } + return true; + } else { + if (isset($dep['conflicts'])) { + return true; + } + if ($required) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires package "' . $depname . '"' . + $extra); + } else { + return $this->warning('warning: %s requires package "' . $depname . '"' . + $extra); + } + } else { + return $this->warning('%s can optionally use package "' . $depname . '"' . + $extra); + } + } + } + if (!$installed && !$downloaded) { + if (isset($dep['conflicts'])) { + return true; + } + if ($required) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires package "' . $depname . '"' . + $extra); + } else { + return $this->warning('warning: %s requires package "' . $depname . '"' . + $extra); + } + } else { + return $this->warning('%s can optionally use package "' . $depname . '"' . + $extra); + } + } + $fail = false; + if (isset($dep['min'])) { + if (version_compare($version, $dep['min'], '<')) { + $fail = true; + } + } + if (isset($dep['max'])) { + if (version_compare($version, $dep['max'], '>')) { + $fail = true; + } + } + if ($fail && !isset($dep['conflicts'])) { + $installed = $installed ? 'installed' : 'downloaded'; + $dep['package'] = $dep['name']; + $dep = $this->_registry->parsedPackageNameToString($dep, true); + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s requires package "' . $depname . '"' . + $extra . ", $installed version is " . $version); + } else { + return $this->warning('warning: %s requires package "' . $depname . '"' . + $extra . ", $installed version is " . $version); + } + } elseif ((isset($dep['min']) || isset($dep['max'])) && !$fail && + isset($dep['conflicts']) && !isset($dep['exclude'])) { + $installed = $installed ? 'installed' : 'downloaded'; + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with package "' . $depname . '"' . $extra . + ", $installed version is " . $version); + } else { + return $this->warning('warning: %s conflicts with package "' . $depname . '"' . + $extra . ", $installed version is " . $version); + } + } + if (isset($dep['exclude'])) { + $installed = $installed ? 'installed' : 'downloaded'; + foreach ($dep['exclude'] as $exclude) { + if (version_compare($version, $exclude, '==') && !isset($dep['conflicts'])) { + if (!isset($this->_options['nodeps']) && + !isset($this->_options['force'])) { + return $this->raiseError('%s is not compatible with ' . + $installed . ' package "' . + $depname . '" version ' . + $exclude); + } else { + return $this->warning('warning: %s is not compatible with ' . + $installed . ' package "' . + $depname . '" version ' . + $exclude); + } + } elseif (version_compare($version, $exclude, '!=') && isset($dep['conflicts'])) { + $installed = $installed ? 'installed' : 'downloaded'; + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with package "' . $depname . '"' . + $extra . ", $installed version is " . $version); + } else { + return $this->warning('warning: %s conflicts with package "' . $depname . '"' . + $extra . ", $installed version is " . $version); + } + } + } + } + if (isset($dep['recommended'])) { + $installed = $installed ? 'installed' : 'downloaded'; + if (version_compare($version, $dep['recommended'], '==')) { + return true; + } else { + if (!$found && $installed) { + $param = $this->_registry->getPackage($dep['name'], $dep['channel']); + } + if ($param) { + $found = false; + foreach ($params as $parent) { + if ($parent->isEqual($this->_currentPackage)) { + $found = true; + break; + } + } + if ($found) { + if ($param->isCompatible($parent)) { + return true; + } + } else { // this is for validPackage() calls + $parent = $this->_registry->getPackage($this->_currentPackage['package'], + $this->_currentPackage['channel']); + if ($parent !== null) { + if ($param->isCompatible($parent)) { + return true; + } + } + } + } + if (!isset($this->_options['nodeps']) && !isset($this->_options['force']) && + !isset($this->_options['loose'])) { + return $this->raiseError('%s dependency package "' . $depname . + '" ' . $installed . ' version ' . $version . + ' is not the recommended version ' . $dep['recommended'] . + ', but may be compatible, use --force to install'); + } else { + return $this->warning('warning: %s dependency package "' . $depname . + '" ' . $installed . ' version ' . $version . + ' is not the recommended version ' . $dep['recommended']); + } + } + } + return true; + } + + function _validatePackageInstall($dep, $required, $depv1 = false) + { + return $this->_validatePackageDownload($dep, $required, array(), $depv1); + } + + /** + * Verify that uninstalling packages passed in to command line is OK. + * + * @param PEAR_Installer $dl + * @return PEAR_Error|true + */ + function validatePackageUninstall(&$dl) + { + if (PEAR::isError($this->_dependencydb)) { + return $this->_dependencydb; + } + $params = array(); + // construct an array of "downloaded" packages to fool the package dependency checker + // into using these to validate uninstalls of circular dependencies + $downloaded = &$dl->getUninstallPackages(); + foreach ($downloaded as $i => $pf) { + if (!class_exists('PEAR_Downloader_Package')) { + require_once 'PEAR/Downloader/Package.php'; + } + $dp = &new PEAR_Downloader_Package($dl); + $dp->setPackageFile($downloaded[$i]); + $params[$i] = &$dp; + } + // check cache + $memyselfandI = strtolower($this->_currentPackage['channel']) . '/' . + strtolower($this->_currentPackage['package']); + if (isset($dl->___uninstall_package_cache)) { + $badpackages = $dl->___uninstall_package_cache; + if (isset($badpackages[$memyselfandI]['warnings'])) { + foreach ($badpackages[$memyselfandI]['warnings'] as $warning) { + $dl->log(0, $warning[0]); + } + } + if (isset($badpackages[$memyselfandI]['errors'])) { + foreach ($badpackages[$memyselfandI]['errors'] as $error) { + if (is_array($error)) { + $dl->log(0, $error[0]); + } else { + $dl->log(0, $error->getMessage()); + } + } + if (isset($this->_options['nodeps']) || isset($this->_options['force'])) { + return $this->warning( + 'warning: %s should not be uninstalled, other installed packages depend ' . + 'on this package'); + } else { + return $this->raiseError( + '%s cannot be uninstalled, other installed packages depend on this package'); + } + } + return true; + } + // first, list the immediate parents of each package to be uninstalled + $perpackagelist = array(); + $allparents = array(); + foreach ($params as $i => $param) { + $a = array('channel' => strtolower($param->getChannel()), + 'package' => strtolower($param->getPackage())); + $deps = $this->_dependencydb->getDependentPackages($a); + if ($deps) { + foreach ($deps as $d) { + $pardeps = $this->_dependencydb->getDependencies($d); + foreach ($pardeps as $dep) { + if (strtolower($dep['dep']['channel']) == $a['channel'] && + strtolower($dep['dep']['name']) == $a['package']) { + if (!isset($perpackagelist[$a['channel'] . '/' . $a['package']])) { + $perpackagelist[$a['channel'] . '/' . $a['package']] = array(); + } + $perpackagelist[$a['channel'] . '/' . $a['package']][] + = array($d['channel'] . '/' . $d['package'], $dep); + if (!isset($allparents[$d['channel'] . '/' . $d['package']])) { + $allparents[$d['channel'] . '/' . $d['package']] = array(); + } + if (!isset($allparents[$d['channel'] . '/' . $d['package']][$a['channel'] . '/' . $a['package']])) { + $allparents[$d['channel'] . '/' . $d['package']][$a['channel'] . '/' . $a['package']] = array(); + } + $allparents[$d['channel'] . '/' . $d['package']] + [$a['channel'] . '/' . $a['package']][] + = array($d, $dep); + } + } + } + } + } + // next, remove any packages from the parents list that are not installed + $remove = array(); + foreach ($allparents as $parent => $d1) { + foreach ($d1 as $d) { + if ($this->_registry->packageExists($d[0][0]['package'], $d[0][0]['channel'])) { + continue; + } + $remove[$parent] = true; + } + } + // next remove any packages from the parents list that are not passed in for + // uninstallation + foreach ($allparents as $parent => $d1) { + foreach ($d1 as $d) { + foreach ($params as $param) { + if (strtolower($param->getChannel()) == $d[0][0]['channel'] && + strtolower($param->getPackage()) == $d[0][0]['package']) { + // found it + continue 3; + } + } + $remove[$parent] = true; + } + } + // remove all packages whose dependencies fail + // save which ones failed for error reporting + $badchildren = array(); + do { + $fail = false; + foreach ($remove as $package => $unused) { + if (!isset($allparents[$package])) { + continue; + } + foreach ($allparents[$package] as $kid => $d1) { + foreach ($d1 as $depinfo) { + if ($depinfo[1]['type'] != 'optional') { + if (isset($badchildren[$kid])) { + continue; + } + $badchildren[$kid] = true; + $remove[$kid] = true; + $fail = true; + continue 2; + } + } + } + if ($fail) { + // start over, we removed some children + continue 2; + } + } + } while ($fail); + // next, construct the list of packages that can't be uninstalled + $badpackages = array(); + $save = $this->_currentPackage; + foreach ($perpackagelist as $package => $packagedeps) { + foreach ($packagedeps as $parent) { + if (!isset($remove[$parent[0]])) { + continue; + } + $packagename = $this->_registry->parsePackageName($parent[0]); + $packagename['channel'] = $this->_registry->channelAlias($packagename['channel']); + $pa = $this->_registry->getPackage($packagename['package'], $packagename['channel']); + $packagename['package'] = $pa->getPackage(); + $this->_currentPackage = $packagename; + // parent is not present in uninstall list, make sure we can actually + // uninstall it (parent dep is optional) + $parentname['channel'] = $this->_registry->channelAlias($parent[1]['dep']['channel']); + $pa = $this->_registry->getPackage($parent[1]['dep']['name'], $parent[1]['dep']['channel']); + $parentname['package'] = $pa->getPackage(); + $parent[1]['dep']['package'] = $parentname['package']; + $parent[1]['dep']['channel'] = $parentname['channel']; + if ($parent[1]['type'] == 'optional') { + $test = $this->_validatePackageUninstall($parent[1]['dep'], false, $dl); + if ($test !== true) { + $badpackages[$package]['warnings'][] = $test; + } + } else { + $test = $this->_validatePackageUninstall($parent[1]['dep'], true, $dl); + if ($test !== true) { + $badpackages[$package]['errors'][] = $test; + } + } + } + } + $this->_currentPackage = $save; + $dl->___uninstall_package_cache = $badpackages; + if (isset($badpackages[$memyselfandI])) { + if (isset($badpackages[$memyselfandI]['warnings'])) { + foreach ($badpackages[$memyselfandI]['warnings'] as $warning) { + $dl->log(0, $warning[0]); + } + } + if (isset($badpackages[$memyselfandI]['errors'])) { + foreach ($badpackages[$memyselfandI]['errors'] as $error) { + if (is_array($error)) { + $dl->log(0, $error[0]); + } else { + $dl->log(0, $error->getMessage()); + } + } + if (isset($this->_options['nodeps']) || isset($this->_options['force'])) { + return $this->warning( + 'warning: %s should not be uninstalled, other installed packages depend ' . + 'on this package'); + } else { + return $this->raiseError( + '%s cannot be uninstalled, other installed packages depend on this package'); + } + } + } + return true; + } + + function _validatePackageUninstall($dep, $required, $dl) + { + $depname = $this->_registry->parsedPackageNameToString($dep, true); + $version = $this->_registry->packageinfo($dep['package'], 'version', + $dep['channel']); + if (!$version) { + return true; + } + $extra = $this->_getExtraString($dep); + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + } + if (isset($dep['conflicts'])) { + return true; // uninstall OK - these packages conflict (probably installed with --force) + } + if (!isset($dep['min']) && !isset($dep['max'])) { + if ($required) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('"' . $depname . '" is required by ' . + 'installed package %s' . $extra); + } else { + return $this->warning('warning: "' . $depname . '" is required by ' . + 'installed package %s' . $extra); + } + } else { + return $this->warning('"' . $depname . '" can be optionally used by ' . + 'installed package %s' . $extra); + } + } + $fail = false; + if (isset($dep['min'])) { + if (version_compare($version, $dep['min'], '>=')) { + $fail = true; + } + } + if (isset($dep['max'])) { + if (version_compare($version, $dep['max'], '<=')) { + $fail = true; + } + } + // we re-use this variable, preserve the original value + $saverequired = $required; + if ($required) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError($depname . $extra . ' is required by installed package' . + ' "%s"'); + } else { + return $this->raiseError('warning: ' . $depname . $extra . + ' is required by installed package "%s"'); + } + } else { + return $this->warning($depname . $extra . ' can be optionally used by installed package' . + ' "%s"'); + } + return true; + } + + /** + * validate a downloaded package against installed packages + * + * As of PEAR 1.4.3, this will only validate + * + * @param array|PEAR_Downloader_Package|PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * $pkg package identifier (either + * array('package' => blah, 'channel' => blah) or an array with + * index 'info' referencing an object) + * @param PEAR_Downloader $dl + * @param array $params full list of packages to install + * @return true|PEAR_Error + */ + function validatePackage($pkg, &$dl, $params = array()) + { + if (is_array($pkg) && isset($pkg['info'])) { + $deps = $this->_dependencydb->getDependentPackageDependencies($pkg['info']); + } else { + $deps = $this->_dependencydb->getDependentPackageDependencies($pkg); + } + $fail = false; + if ($deps) { + if (!class_exists('PEAR_Downloader_Package')) { + require_once 'PEAR/Downloader/Package.php'; + } + $dp = &new PEAR_Downloader_Package($dl); + if (is_object($pkg)) { + $dp->setPackageFile($pkg); + } else { + $dp->setDownloadURL($pkg); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + foreach ($deps as $channel => $info) { + foreach ($info as $package => $ds) { + foreach ($params as $packd) { + if (strtolower($packd->getPackage()) == strtolower($package) && + $packd->getChannel() == $channel) { + $dl->log(3, 'skipping installed package check of "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $channel, 'package' => $package), + true) . + '", version "' . $packd->getVersion() . '" will be ' . + 'downloaded and installed'); + continue 2; // jump to next package + } + } + foreach ($ds as $d) { + $checker = &new PEAR_Dependency2($this->_config, $this->_options, + array('channel' => $channel, 'package' => $package), $this->_state); + $dep = $d['dep']; + $required = $d['type'] == 'required'; + $ret = $checker->_validatePackageDownload($dep, $required, array(&$dp)); + if (is_array($ret)) { + $dl->log(0, $ret[0]); + } elseif (PEAR::isError($ret)) { + $dl->log(0, $ret->getMessage()); + $fail = true; + } + } + } + } + PEAR::popErrorHandling(); + } + if ($fail) { + return $this->raiseError( + '%s cannot be installed, conflicts with installed packages'); + } + return true; + } + + /** + * validate a package.xml 1.0 dependency + */ + function validateDependency1($dep, $params = array()) + { + if (!isset($dep['optional'])) { + $dep['optional'] = 'no'; + } + list($newdep, $type) = $this->normalizeDep($dep); + if (!$newdep) { + return $this->raiseError("Invalid Dependency"); + } + if (method_exists($this, "validate{$type}Dependency")) { + return $this->{"validate{$type}Dependency"}($newdep, $dep['optional'] == 'no', + $params, true); + } + } + + /** + * Convert a 1.0 dep into a 2.0 dep + */ + function normalizeDep($dep) + { + $types = array( + 'pkg' => 'Package', + 'ext' => 'Extension', + 'os' => 'Os', + 'php' => 'Php' + ); + if (isset($types[$dep['type']])) { + $type = $types[$dep['type']]; + } else { + return array(false, false); + } + $newdep = array(); + switch ($type) { + case 'Package' : + $newdep['channel'] = 'pear.php.net'; + case 'Extension' : + case 'Os' : + $newdep['name'] = $dep['name']; + break; + } + $dep['rel'] = PEAR_Dependency2::signOperator($dep['rel']); + switch ($dep['rel']) { + case 'has' : + return array($newdep, $type); + break; + case 'not' : + $newdep['conflicts'] = true; + break; + case '>=' : + case '>' : + $newdep['min'] = $dep['version']; + if ($dep['rel'] == '>') { + $newdep['exclude'] = $dep['version']; + } + break; + case '<=' : + case '<' : + $newdep['max'] = $dep['version']; + if ($dep['rel'] == '<') { + $newdep['exclude'] = $dep['version']; + } + break; + case 'ne' : + case '!=' : + $newdep['min'] = '0'; + $newdep['max'] = '100000'; + $newdep['exclude'] = $dep['version']; + break; + case '==' : + $newdep['min'] = $dep['version']; + $newdep['max'] = $dep['version']; + break; + } + if ($type == 'Php') { + if (!isset($newdep['min'])) { + $newdep['min'] = '4.2.0'; + } + if (!isset($newdep['max'])) { + $newdep['max'] = '6.0.0'; + } + } + return array($newdep, $type); + } + + /** + * Converts text comparing operators to them sign equivalents + * + * Example: 'ge' to '>=' + * + * @access public + * @param string Operator + * @return string Sign equivalent + */ + function signOperator($operator) + { + switch($operator) { + case 'lt': return '<'; + case 'le': return '<='; + case 'gt': return '>'; + case 'ge': return '>='; + case 'eq': return '=='; + case 'ne': return '!='; + default: + return $operator; + } + } + + function raiseError($msg) + { + if (isset($this->_options['ignore-errors'])) { + return $this->warning($msg); + } + return PEAR::raiseError(sprintf($msg, $this->_registry->parsedPackageNameToString( + $this->_currentPackage, true))); + } + + function warning($msg) + { + return array(sprintf($msg, $this->_registry->parsedPackageNameToString( + $this->_currentPackage, true))); + } +} +?> \ No newline at end of file diff --git a/PEAR/DependencyDB.php b/PEAR/DependencyDB.php new file mode 100644 index 0000000..fa293ca --- /dev/null +++ b/PEAR/DependencyDB.php @@ -0,0 +1,707 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: DependencyDB.php,v 1.35 2007/01/06 04:03:32 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Needed for error handling + */ +require_once 'PEAR.php'; +require_once 'PEAR/Config.php'; + +$GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] = array(); +/** + * Track dependency relationships between installed packages + * @category pear + * @package PEAR + * @author Greg Beaver + * @author Tomas V.V.Cox + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_DependencyDB +{ + // {{{ properties + + /** + * This is initialized by {@link setConfig()} + * @var PEAR_Config + * @access private + */ + var $_config; + /** + * This is initialized by {@link setConfig()} + * @var PEAR_Registry + * @access private + */ + var $_registry; + /** + * Filename of the dependency DB (usually .depdb) + * @var string + * @access private + */ + var $_depdb = false; + /** + * File name of the lockfile (usually .depdblock) + * @var string + * @access private + */ + var $_lockfile = false; + /** + * Open file resource for locking the lockfile + * @var resource|false + * @access private + */ + var $_lockFp = false; + /** + * API version of this class, used to validate a file on-disk + * @var string + * @access private + */ + var $_version = '1.0'; + /** + * Cached dependency database file + * @var array|null + * @access private + */ + var $_cache; + + // }}} + // {{{ & singleton() + + /** + * Get a raw dependency database. Calls setConfig() and assertDepsDB() + * @param PEAR_Config + * @param string|false full path to the dependency database, or false to use default + * @return PEAR_DependencyDB|PEAR_Error + * @static + */ + function &singleton(&$config, $depdb = false) + { + if (!isset($GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] + [$config->get('php_dir', null, 'pear.php.net')])) { + $a = new PEAR_DependencyDB; + $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] + [$config->get('php_dir', null, 'pear.php.net')] = &$a; + $a->setConfig($config, $depdb); + if (PEAR::isError($e = $a->assertDepsDB())) { + return $e; + } + } + return $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] + [$config->get('php_dir', null, 'pear.php.net')]; + } + + /** + * Set up the registry/location of dependency DB + * @param PEAR_Config|false + * @param string|false full path to the dependency database, or false to use default + */ + function setConfig(&$config, $depdb = false) + { + if (!$config) { + $this->_config = &PEAR_Config::singleton(); + } else { + $this->_config = &$config; + } + $this->_registry = &$this->_config->getRegistry(); + if (!$depdb) { + $this->_depdb = $this->_config->get('php_dir', null, 'pear.php.net') . + DIRECTORY_SEPARATOR . '.depdb'; + } else { + $this->_depdb = $depdb; + } + $this->_lockfile = dirname($this->_depdb) . DIRECTORY_SEPARATOR . '.depdblock'; + } + // }}} + + function hasWriteAccess() + { + if (!file_exists($this->_depdb)) { + $dir = $this->_depdb; + while ($dir && $dir != '.') { + $dir = dirname($dir); // cd .. + if ($dir != '.' && file_exists($dir)) { + if (is_writeable($dir)) { + return true; + } else { + return false; + } + } + } + return false; + } + return is_writeable($this->_depdb); + } + + // {{{ assertDepsDB() + + /** + * Create the dependency database, if it doesn't exist. Error if the database is + * newer than the code reading it. + * @return void|PEAR_Error + */ + function assertDepsDB() + { + if (!is_file($this->_depdb)) { + $this->rebuildDB(); + } else { + $depdb = $this->_getDepDB(); + // Datatype format has been changed, rebuild the Deps DB + if ($depdb['_version'] < $this->_version) { + $this->rebuildDB(); + } + if ($depdb['_version']{0} > $this->_version{0}) { + return PEAR::raiseError('Dependency database is version ' . + $depdb['_version'] . ', and we are version ' . + $this->_version . ', cannot continue'); + } + } + } + + /** + * Get a list of installed packages that depend on this package + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array + * @return array|false + */ + function getDependentPackages(&$pkg) + { + $data = $this->_getDepDB(); + if (is_object($pkg)) { + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + } else { + $channel = strtolower($pkg['channel']); + $package = strtolower($pkg['package']); + } + if (isset($data['packages'][$channel][$package])) { + return $data['packages'][$channel][$package]; + } + return false; + } + + /** + * Get a list of the actual dependencies of installed packages that depend on + * a package. + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array + * @return array|false + */ + function getDependentPackageDependencies(&$pkg) + { + $data = $this->_getDepDB(); + if (is_object($pkg)) { + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + } else { + $channel = strtolower($pkg['channel']); + $package = strtolower($pkg['package']); + } + $depend = $this->getDependentPackages($pkg); + if (!$depend) { + return false; + } + $dependencies = array(); + foreach ($depend as $info) { + $temp = $this->getDependencies($info); + foreach ($temp as $dep) { + if (strtolower($dep['dep']['channel']) == strtolower($channel) && + strtolower($dep['dep']['name']) == strtolower($package)) { + if (!isset($dependencies[$info['channel']])) { + $dependencies[$info['channel']] = array(); + } + if (!isset($dependencies[$info['channel']][$info['package']])) { + $dependencies[$info['channel']][$info['package']] = array(); + } + $dependencies[$info['channel']][$info['package']][] = $dep; + } + } + } + return $dependencies; + } + + /** + * Get a list of dependencies of this installed package + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array + * @return array|false + */ + function getDependencies(&$pkg) + { + if (is_object($pkg)) { + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + } else { + $channel = strtolower($pkg['channel']); + $package = strtolower($pkg['package']); + } + $data = $this->_getDepDB(); + if (isset($data['dependencies'][$channel][$package])) { + return $data['dependencies'][$channel][$package]; + } + return false; + } + + /** + * Determine whether $parent depends on $child, near or deep + * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2 + * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2 + */ + function dependsOn($parent, $child) + { + $c = array(); + $this->_getDepDB(); + return $this->_dependsOn($parent, $child, $c); + } + + function _dependsOn($parent, $child, &$checked) + { + if (is_object($parent)) { + $channel = strtolower($parent->getChannel()); + $package = strtolower($parent->getPackage()); + } else { + $channel = strtolower($parent['channel']); + $package = strtolower($parent['package']); + } + if (is_object($child)) { + $depchannel = strtolower($child->getChannel()); + $deppackage = strtolower($child->getPackage()); + } else { + $depchannel = strtolower($child['channel']); + $deppackage = strtolower($child['package']); + } + if (isset($checked[$channel][$package][$depchannel][$deppackage])) { + return false; // avoid endless recursion + } + $checked[$channel][$package][$depchannel][$deppackage] = true; + if (!isset($this->_cache['dependencies'][$channel][$package])) { + return false; + } + foreach ($this->_cache['dependencies'][$channel][$package] as $info) { + if (isset($info['dep']['uri'])) { + if (is_object($child)) { + if ($info['dep']['uri'] == $child->getURI()) { + return true; + } + } elseif (isset($child['uri'])) { + if ($info['dep']['uri'] == $child['uri']) { + return true; + } + } + return false; + } + if (strtolower($info['dep']['channel']) == strtolower($depchannel) && + strtolower($info['dep']['name']) == strtolower($deppackage)) { + return true; + } + } + foreach ($this->_cache['dependencies'][$channel][$package] as $info) { + if (isset($info['dep']['uri'])) { + if ($this->_dependsOn(array( + 'uri' => $info['dep']['uri'], + 'package' => $info['dep']['name']), $child, $checked)) { + return true; + } + } else { + if ($this->_dependsOn(array( + 'channel' => $info['dep']['channel'], + 'package' => $info['dep']['name']), $child, $checked)) { + return true; + } + } + } + return false; + } + + /** + * Register dependencies of a package that is being installed or upgraded + * @param PEAR_PackageFile_v2|PEAR_PackageFile_v2 + */ + function installPackage(&$package) + { + $data = $this->_getDepDB(); + unset($this->_cache); + $this->_setPackageDeps($data, $package); + $this->_writeDepDB($data); + } + + /** + * Remove dependencies of a package that is being uninstalled, or upgraded. + * + * Upgraded packages first uninstall, then install + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array If an array, then it must have + * indices 'channel' and 'package' + */ + function uninstallPackage(&$pkg) + { + $data = $this->_getDepDB(); + unset($this->_cache); + if (is_object($pkg)) { + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + } else { + $channel = strtolower($pkg['channel']); + $package = strtolower($pkg['package']); + } + if (!isset($data['dependencies'][$channel][$package])) { + return true; + } + foreach ($data['dependencies'][$channel][$package] as $dep) { + $found = false; + if (isset($dep['dep']['uri'])) { + $depchannel = '__uri'; + } else { + $depchannel = strtolower($dep['dep']['channel']); + } + if (isset($data['packages'][$depchannel][strtolower($dep['dep']['name'])])) { + foreach ($data['packages'][$depchannel][strtolower($dep['dep']['name'])] as + $i => $info) { + if ($info['channel'] == $channel && + $info['package'] == $package) { + $found = true; + break; + } + } + } + if ($found) { + unset($data['packages'][$depchannel][strtolower($dep['dep']['name'])][$i]); + if (!count($data['packages'][$depchannel][strtolower($dep['dep']['name'])])) { + unset($data['packages'][$depchannel][strtolower($dep['dep']['name'])]); + if (!count($data['packages'][$depchannel])) { + unset($data['packages'][$depchannel]); + } + } else { + $data['packages'][$depchannel][strtolower($dep['dep']['name'])] = + array_values( + $data['packages'][$depchannel][strtolower($dep['dep']['name'])]); + } + } + } + unset($data['dependencies'][$channel][$package]); + if (!count($data['dependencies'][$channel])) { + unset($data['dependencies'][$channel]); + } + if (!count($data['dependencies'])) { + unset($data['dependencies']); + } + if (!count($data['packages'])) { + unset($data['packages']); + } + $this->_writeDepDB($data); + } + + /** + * Rebuild the dependency DB by reading registry entries. + * @return true|PEAR_Error + */ + function rebuildDB() + { + $depdb = array('_version' => $this->_version); + if (!$this->hasWriteAccess()) { + // allow startup for read-only with older Registry + return $depdb; + } + $packages = $this->_registry->listAllPackages(); + foreach ($packages as $channel => $ps) { + foreach ($ps as $package) { + $package = $this->_registry->getPackage($package, $channel); + $this->_setPackageDeps($depdb, $package); + } + } + $error = $this->_writeDepDB($depdb); + if (PEAR::isError($error)) { + return $error; + } + $this->_cache = $depdb; + return true; + } + + /** + * Register usage of the dependency DB to prevent race conditions + * @param int one of the LOCK_* constants + * @return true|PEAR_Error + * @access private + */ + function _lock($mode = LOCK_EX) + { + if (!eregi('Windows 9', php_uname())) { + if ($mode != LOCK_UN && is_resource($this->_lockFp)) { + // XXX does not check type of lock (LOCK_SH/LOCK_EX) + return true; + } + $open_mode = 'w'; + // XXX People reported problems with LOCK_SH and 'w' + if ($mode === LOCK_SH) { + if (!file_exists($this->_lockfile)) { + touch($this->_lockfile); + } elseif (!is_file($this->_lockfile)) { + return PEAR::raiseError('could not create Dependency lock file, ' . + 'it exists and is not a regular file'); + } + $open_mode = 'r'; + } + + if (!is_resource($this->_lockFp)) { + $this->_lockFp = @fopen($this->_lockfile, $open_mode); + } + if (!is_resource($this->_lockFp)) { + return PEAR::raiseError("could not create Dependency lock file" . + (isset($php_errormsg) ? ": " . $php_errormsg : "")); + } + if (!(int)flock($this->_lockFp, $mode)) { + switch ($mode) { + case LOCK_SH: $str = 'shared'; break; + case LOCK_EX: $str = 'exclusive'; break; + case LOCK_UN: $str = 'unlock'; break; + default: $str = 'unknown'; break; + } + return PEAR::raiseError("could not acquire $str lock ($this->_lockfile)"); + } + } + return true; + } + + /** + * Release usage of dependency DB + * @return true|PEAR_Error + * @access private + */ + function _unlock() + { + $ret = $this->_lock(LOCK_UN); + if (is_resource($this->_lockFp)) { + fclose($this->_lockFp); + } + $this->_lockFp = null; + return $ret; + } + + /** + * Load the dependency database from disk, or return the cache + * @return array|PEAR_Error + */ + function _getDepDB() + { + if (!$this->hasWriteAccess()) { + return array('_version' => $this->_version); + } + if (isset($this->_cache)) { + return $this->_cache; + } + if (!$fp = fopen($this->_depdb, 'r')) { + $err = PEAR::raiseError("Could not open dependencies file `".$this->_depdb."'"); + return $err; + } + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + clearstatcache(); + fclose($fp); + $data = unserialize(file_get_contents($this->_depdb)); + set_magic_quotes_runtime($rt); + $this->_cache = $data; + return $data; + } + + /** + * Write out the dependency database to disk + * @param array the database + * @return true|PEAR_Error + * @access private + */ + function _writeDepDB(&$deps) + { + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + if (!$fp = fopen($this->_depdb, 'wb')) { + $this->_unlock(); + return PEAR::raiseError("Could not open dependencies file `".$this->_depdb."' for writing"); + } + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + fwrite($fp, serialize($deps)); + set_magic_quotes_runtime($rt); + fclose($fp); + $this->_unlock(); + $this->_cache = $deps; + return true; + } + + /** + * Register all dependencies from a package in the dependencies database, in essence + * "installing" the package's dependency information + * @param array the database + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @access private + */ + function _setPackageDeps(&$data, &$pkg) + { + $pkg->setConfig($this->_config); + if ($pkg->getPackagexmlVersion() == '1.0') { + $gen = &$pkg->getDefaultGenerator(); + $deps = $gen->dependenciesToV2(); + } else { + $deps = $pkg->getDeps(true); + } + if (!$deps) { + return; + } + if (!is_array($data)) { + $data = array(); + } + if (!isset($data['dependencies'])) { + $data['dependencies'] = array(); + } + if (!isset($data['dependencies'][strtolower($pkg->getChannel())])) { + $data['dependencies'][strtolower($pkg->getChannel())] = array(); + } + $data['dependencies'][strtolower($pkg->getChannel())][strtolower($pkg->getPackage())] + = array(); + if (isset($deps['required']['package'])) { + if (!isset($deps['required']['package'][0])) { + $deps['required']['package'] = array($deps['required']['package']); + } + foreach ($deps['required']['package'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'required'); + } + } + if (isset($deps['optional']['package'])) { + if (!isset($deps['optional']['package'][0])) { + $deps['optional']['package'] = array($deps['optional']['package']); + } + foreach ($deps['optional']['package'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'optional'); + } + } + if (isset($deps['required']['subpackage'])) { + if (!isset($deps['required']['subpackage'][0])) { + $deps['required']['subpackage'] = array($deps['required']['subpackage']); + } + foreach ($deps['required']['subpackage'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'required'); + } + } + if (isset($deps['optional']['subpackage'])) { + if (!isset($deps['optional']['subpackage'][0])) { + $deps['optional']['subpackage'] = array($deps['optional']['subpackage']); + } + foreach ($deps['optional']['subpackage'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'optional'); + } + } + if (isset($deps['group'])) { + if (!isset($deps['group'][0])) { + $deps['group'] = array($deps['group']); + } + foreach ($deps['group'] as $group) { + if (isset($group['package'])) { + if (!isset($group['package'][0])) { + $group['package'] = array($group['package']); + } + foreach ($group['package'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'optional', + $group['attribs']['name']); + } + } + if (isset($group['subpackage'])) { + if (!isset($group['subpackage'][0])) { + $group['subpackage'] = array($group['subpackage']); + } + foreach ($group['subpackage'] as $dep) { + $this->_registerDep($data, $pkg, $dep, 'optional', + $group['attribs']['name']); + } + } + } + } + if ($data['dependencies'][strtolower($pkg->getChannel())] + [strtolower($pkg->getPackage())] == array()) { + unset($data['dependencies'][strtolower($pkg->getChannel())] + [strtolower($pkg->getPackage())]); + if (!count($data['dependencies'][strtolower($pkg->getChannel())])) { + unset($data['dependencies'][strtolower($pkg->getChannel())]); + } + } + } + + /** + * @param array the database + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param array the specific dependency + * @param required|optional whether this is a required or an optional dep + * @param string|false dependency group this dependency is from, or false for ordinary dep + */ + function _registerDep(&$data, &$pkg, $dep, $type, $group = false) + { + $info = array( + 'dep' => $dep, + 'type' => $type, + 'group' => $group); + + if (isset($dep['channel'])) { + $depchannel = $dep['channel']; + } else { + $depchannel = '__uri'; + } + if (!isset($data['dependencies'])) { + $data['dependencies'] = array(); + } + if (!isset($data['dependencies'][strtolower($pkg->getChannel())])) { + $data['dependencies'][strtolower($pkg->getChannel())] = array(); + } + if (!isset($data['dependencies'][strtolower($pkg->getChannel())][strtolower($pkg->getPackage())])) { + $data['dependencies'][strtolower($pkg->getChannel())][strtolower($pkg->getPackage())] = array(); + } + $data['dependencies'][strtolower($pkg->getChannel())][strtolower($pkg->getPackage())][] + = $info; + if (isset($data['packages'][strtolower($depchannel)][strtolower($dep['name'])])) { + $found = false; + foreach ($data['packages'][strtolower($depchannel)][strtolower($dep['name'])] + as $i => $p) { + if ($p['channel'] == strtolower($pkg->getChannel()) && + $p['package'] == strtolower($pkg->getPackage())) { + $found = true; + break; + } + } + if (!$found) { + $data['packages'][strtolower($depchannel)][strtolower($dep['name'])][] + = array('channel' => strtolower($pkg->getChannel()), + 'package' => strtolower($pkg->getPackage())); + } + } else { + if (!isset($data['packages'])) { + $data['packages'] = array(); + } + if (!isset($data['packages'][strtolower($depchannel)])) { + $data['packages'][strtolower($depchannel)] = array(); + } + if (!isset($data['packages'][strtolower($depchannel)][strtolower($dep['name'])])) { + $data['packages'][strtolower($depchannel)][strtolower($dep['name'])] = array(); + } + $data['packages'][strtolower($depchannel)][strtolower($dep['name'])][] + = array('channel' => strtolower($pkg->getChannel()), + 'package' => strtolower($pkg->getPackage())); + } + } +} +?> \ No newline at end of file diff --git a/PEAR/Downloader.php b/PEAR/Downloader.php new file mode 100644 index 0000000..f8e9c1e --- /dev/null +++ b/PEAR/Downloader.php @@ -0,0 +1,1746 @@ + + * @author Stig Bakken + * @author Tomas V. V. Cox + * @author Martin Jansen + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Downloader.php,v 1.132 2007/06/11 05:30:38 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.3.0 + */ + +/** + * Needed for constants, extending + */ +require_once 'PEAR/Common.php'; + +define('PEAR_INSTALLER_OK', 1); +define('PEAR_INSTALLER_FAILED', 0); +define('PEAR_INSTALLER_SKIPPED', -1); +define('PEAR_INSTALLER_ERROR_NO_PREF_STATE', 2); + +/** + * Administration class used to download anything from the internet (PEAR Packages, + * static URLs, xml files) + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @author Stig Bakken + * @author Tomas V. V. Cox + * @author Martin Jansen + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.3.0 + */ +class PEAR_Downloader extends PEAR_Common +{ + /** + * @var PEAR_Registry + * @access private + */ + var $_registry; + + /** + * @var PEAR_Remote + * @access private + */ + var $_remote; + + /** + * Preferred Installation State (snapshot, devel, alpha, beta, stable) + * @var string|null + * @access private + */ + var $_preferredState; + + /** + * Options from command-line passed to Install. + * + * Recognized options:
+ * - onlyreqdeps : install all required dependencies as well + * - alldeps : install all dependencies, including optional + * - installroot : base relative path to install files in + * - force : force a download even if warnings would prevent it + * - nocompress : download uncompressed tarballs + * @see PEAR_Command_Install + * @access private + * @var array + */ + var $_options; + + /** + * Downloaded Packages after a call to download(). + * + * Format of each entry: + * + * + * array('pkg' => 'package_name', 'file' => '/path/to/local/file', + * 'info' => array() // parsed package.xml + * ); + * + * @access private + * @var array + */ + var $_downloadedPackages = array(); + + /** + * Packages slated for download. + * + * This is used to prevent downloading a package more than once should it be a dependency + * for two packages to be installed. + * Format of each entry: + * + *
+     * array('package_name1' => parsed package.xml, 'package_name2' => parsed package.xml,
+     * );
+     * 
+ * @access private + * @var array + */ + var $_toDownload = array(); + + /** + * Array of every package installed, with names lower-cased. + * + * Format: + * + * array('package1' => 0, 'package2' => 1, ); + * + * @var array + */ + var $_installed = array(); + + /** + * @var array + * @access private + */ + var $_errorStack = array(); + + /** + * @var boolean + * @access private + */ + var $_internalDownload = false; + + /** + * Temporary variable used in sorting packages by dependency in {@link sortPkgDeps()} + * @var array + * @access private + */ + var $_packageSortTree; + + /** + * Temporary directory, or configuration value where downloads will occur + * @var string + */ + var $_downloadDir; + // {{{ PEAR_Downloader() + + /** + * @param PEAR_Frontend_* + * @param array + * @param PEAR_Config + */ + function PEAR_Downloader(&$ui, $options, &$config) + { + parent::PEAR_Common(); + $this->_options = $options; + $this->config = &$config; + $this->_preferredState = $this->config->get('preferred_state'); + $this->ui = &$ui; + if (!$this->_preferredState) { + // don't inadvertantly use a non-set preferred_state + $this->_preferredState = null; + } + + if (isset($this->_options['installroot'])) { + $this->config->setInstallRoot($this->_options['installroot']); + } + $this->_registry = &$config->getRegistry(); + $this->_remote = &$config->getRemote(); + + if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) { + $this->_installed = $this->_registry->listAllPackages(); + foreach ($this->_installed as $key => $unused) { + if (!count($unused)) { + continue; + } + $strtolower = create_function('$a','return strtolower($a);'); + array_walk($this->_installed[$key], $strtolower); + } + } + } + + /** + * Attempt to discover a channel's remote capabilities from + * its server name + * @param string + * @return boolean + */ + function discover($channel) + { + $this->log(1, 'Attempting to discover channel "' . $channel . '"...'); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $callback = $this->ui ? array(&$this, '_downloadCallback') : null; + if (!class_exists('System')) { + require_once 'System.php'; + } + $a = $this->downloadHttp('http://' . $channel . '/channel.xml', $this->ui, + System::mktemp(array('-d')), $callback, false); + PEAR::popErrorHandling(); + if (PEAR::isError($a)) { + return false; + } + list($a, $lastmodified) = $a; + if (!class_exists('PEAR/ChannelFile.php')) { + require_once 'PEAR/ChannelFile.php'; + } + $b = new PEAR_ChannelFile; + if ($b->fromXmlFile($a)) { + unlink($a); + if ($this->config->get('auto_discover')) { + $this->_registry->addChannel($b, $lastmodified); + $alias = $b->getName(); + if ($b->getName() == $this->_registry->channelName($b->getAlias())) { + $alias = $b->getAlias(); + } + $this->log(1, 'Auto-discovered channel "' . $channel . + '", alias "' . $alias . '", adding to registry'); + } + return true; + } + unlink($a); + return false; + } + + /** + * For simpler unit-testing + * @param PEAR_Downloader + * @return PEAR_Downloader_Package + */ + function &newDownloaderPackage(&$t) + { + if (!class_exists('PEAR_Downloader_Package')) { + require_once 'PEAR/Downloader/Package.php'; + } + $a = &new PEAR_Downloader_Package($t); + return $a; + } + + /** + * For simpler unit-testing + * @param PEAR_Config + * @param array + * @param array + * @param int + */ + function &getDependency2Object(&$c, $i, $p, $s) + { + if (!class_exists('PEAR/Dependency2.php')) { + require_once 'PEAR/Dependency2.php'; + } + $z = &new PEAR_Dependency2($c, $i, $p, $s); + return $z; + } + + function &download($params) + { + if (!count($params)) { + $a = array(); + return $a; + } + if (!isset($this->_registry)) { + $this->_registry = &$this->config->getRegistry(); + } + if (!isset($this->_remote)) { + $this->_remote = &$this->config->getRemote(); + } + $channelschecked = array(); + // convert all parameters into PEAR_Downloader_Package objects + foreach ($params as $i => $param) { + $params[$i] = &$this->newDownloaderPackage($this); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = $params[$i]->initialize($param); + PEAR::staticPopErrorHandling(); + if (!$err) { + // skip parameters that were missed by preferred_state + continue; + } + if (PEAR::isError($err)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $err->getMessage()); + } + $params[$i] = false; + if (is_object($param)) { + $param = $param->getChannel() . '/' . $param->getPackage(); + } + $this->pushError('Package "' . $param . '" is not valid', + PEAR_INSTALLER_SKIPPED); + } else { + do { + if ($params[$i] && $params[$i]->getType() == 'local') { + // bug #7090 + // skip channel.xml check for local packages + break; + } + if ($params[$i] && !isset($channelschecked[$params[$i]->getChannel()]) && + !isset($this->_options['offline'])) { + $channelschecked[$params[$i]->getChannel()] = true; + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (!class_exists('System')) { + require_once 'System.php'; + } + $curchannel = &$this->_registry->getChannel($params[$i]->getChannel()); + if (PEAR::isError($curchannel)) { + PEAR::staticPopErrorHandling(); + return $this->raiseError($curchannel); + } + if (PEAR::isError($dir = $this->getDownloadDir())) { + PEAR::staticPopErrorHandling(); + break; + } + $mirror = $this->config->get('preferred_mirror', null, + $params[$i]->getChannel()); + $a = $this->downloadHttp('http://' . $mirror . + '/channel.xml', $this->ui, $dir, null, $curchannel->lastModified()); + + PEAR::staticPopErrorHandling(); + if (PEAR::isError($a) || !$a) { + break; + } + $this->log(0, 'WARNING: channel "' . $params[$i]->getChannel() . '" has ' . + 'updated its protocols, use "channel-update ' . $params[$i]->getChannel() . + '" to update'); + } + } while (false); + if ($params[$i] && !isset($this->_options['downloadonly'])) { + if (isset($this->_options['packagingroot'])) { + $checkdir = $this->_prependPath( + $this->config->get('php_dir', null, $params[$i]->getChannel()), + $this->_options['packagingroot']); + } else { + $checkdir = $this->config->get('php_dir', + null, $params[$i]->getChannel()); + } + while ($checkdir && $checkdir != '/' && !file_exists($checkdir)) { + $checkdir = dirname($checkdir); + } + if ($checkdir == '.') { + $checkdir = '/'; + } + if (!is_writeable($checkdir)) { + return PEAR::raiseError('Cannot install, php_dir for channel "' . + $params[$i]->getChannel() . '" is not writeable by the current user'); + } + } + } + } + unset($channelschecked); + PEAR_Downloader_Package::removeDuplicates($params); + if (!count($params)) { + $a = array(); + return $a; + } + if (!isset($this->_options['nodeps']) && !isset($this->_options['offline'])) { + $reverify = true; + while ($reverify) { + $reverify = false; + foreach ($params as $i => $param) { + //PHP Bug 40768 / PEAR Bug #10944 + //Nested foreaches fail in PHP 5.2.1 + key($params); + $ret = $params[$i]->detectDependencies($params); + if (PEAR::isError($ret)) { + $reverify = true; + $params[$i] = false; + PEAR_Downloader_Package::removeDuplicates($params); + if (!isset($this->_options['soft'])) { + $this->log(0, $ret->getMessage()); + } + continue 2; + } + } + } + } + if (isset($this->_options['offline'])) { + $this->log(3, 'Skipping dependency download check, --offline specified'); + } + if (!count($params)) { + $a = array(); + return $a; + } + while (PEAR_Downloader_Package::mergeDependencies($params)); + PEAR_Downloader_Package::removeDuplicates($params, true); + $errorparams = array(); + if (PEAR_Downloader_Package::detectStupidDuplicates($params, $errorparams)) { + if (count($errorparams)) { + foreach ($errorparams as $param) { + $name = $this->_registry->parsedPackageNameToString($param->getParsedPackage()); + $this->pushError('Duplicate package ' . $name . ' found', PEAR_INSTALLER_FAILED); + } + $a = array(); + return $a; + } + } + PEAR_Downloader_Package::removeInstalled($params); + if (!count($params)) { + $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED); + $a = array(); + return $a; + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->analyzeDependencies($params); + PEAR::popErrorHandling(); + if (!count($params)) { + $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED); + $a = array(); + return $a; + } + $ret = array(); + $newparams = array(); + if (isset($this->_options['pretend'])) { + return $params; + } + $somefailed = false; + foreach ($params as $i => $package) { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pf = &$params[$i]->download(); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pf)) { + if (!isset($this->_options['soft'])) { + $this->log(1, $pf->getMessage()); + $this->log(0, 'Error: cannot download "' . + $this->_registry->parsedPackageNameToString($package->getParsedPackage(), + true) . + '"'); + } + $somefailed = true; + continue; + } + $newparams[] = &$params[$i]; + $ret[] = array('file' => $pf->getArchiveFile(), + 'info' => &$pf, + 'pkg' => $pf->getPackage()); + } + if ($somefailed) { + // remove params that did not download successfully + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->analyzeDependencies($newparams, true); + PEAR::popErrorHandling(); + if (!count($newparams)) { + $this->pushError('Download failed', PEAR_INSTALLER_FAILED); + $a = array(); + return $a; + } + } + $this->_downloadedPackages = $ret; + return $newparams; + } + + /** + * @param array all packages to be installed + */ + function analyzeDependencies(&$params, $force = false) + { + $hasfailed = $failed = false; + if (isset($this->_options['downloadonly'])) { + return; + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $redo = true; + $reset = false; + while ($redo) { + $redo = false; + foreach ($params as $i => $param) { + $deps = $param->getDeps(); + if (!$deps) { + $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(), + $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING); + if ($param->getType() == 'xmlrpc') { + $send = $param->getDownloadURL(); + } else { + $send = $param->getPackageFile(); + } + $installcheck = $depchecker->validatePackage($send, $this, $params); + if (PEAR::isError($installcheck)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $installcheck->getMessage()); + } + $hasfailed = true; + $params[$i] = false; + $reset = true; + $redo = true; + $failed = false; + PEAR_Downloader_Package::removeDuplicates($params); + continue 2; + } + continue; + } + if (!$reset && $param->alreadyValidated() && !$force) { + continue; + } + if (count($deps)) { + $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(), + $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING); + if ($param->getType() == 'xmlrpc') { + $send = $param->getDownloadURL(); + } else { + $send = $param->getPackageFile(); + } + $installcheck = $depchecker->validatePackage($send, $this, $params); + if (PEAR::isError($installcheck)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $installcheck->getMessage()); + } + $hasfailed = true; + $params[$i] = false; + $reset = true; + $redo = true; + $failed = false; + PEAR_Downloader_Package::removeDuplicates($params); + continue 2; + } + $failed = false; + if (isset($deps['required'])) { + foreach ($deps['required'] as $type => $dep) { + // note: Dependency2 will never return a PEAR_Error if ignore-errors + // is specified, so soft is needed to turn off logging + if (!isset($dep[0])) { + if (PEAR::isError($e = $depchecker->{"validate{$type}Dependency"}($dep, + true, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } else { + foreach ($dep as $d) { + if (PEAR::isError($e = + $depchecker->{"validate{$type}Dependency"}($d, + true, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } + } + } + if (isset($deps['optional'])) { + foreach ($deps['optional'] as $type => $dep) { + if (!isset($dep[0])) { + if (PEAR::isError($e = + $depchecker->{"validate{$type}Dependency"}($dep, + false, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } else { + foreach ($dep as $d) { + if (PEAR::isError($e = + $depchecker->{"validate{$type}Dependency"}($d, + false, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } + } + } + } + $groupname = $param->getGroup(); + if (isset($deps['group']) && $groupname) { + if (!isset($deps['group'][0])) { + $deps['group'] = array($deps['group']); + } + $found = false; + foreach ($deps['group'] as $group) { + if ($group['attribs']['name'] == $groupname) { + $found = true; + break; + } + } + if ($found) { + unset($group['attribs']); + foreach ($group as $type => $dep) { + if (!isset($dep[0])) { + if (PEAR::isError($e = + $depchecker->{"validate{$type}Dependency"}($dep, + false, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } else { + foreach ($dep as $d) { + if (PEAR::isError($e = + $depchecker->{"validate{$type}Dependency"}($d, + false, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } + } + } + } + } + } else { + foreach ($deps as $dep) { + if (PEAR::isError($e = $depchecker->validateDependency1($dep, $params))) { + $failed = true; + if (!isset($this->_options['soft'])) { + $this->log(0, $e->getMessage()); + } + } elseif (is_array($e) && !$param->alreadyValidated()) { + if (!isset($this->_options['soft'])) { + $this->log(0, $e[0]); + } + } + } + } + $params[$i]->setValidated(); + } + if ($failed) { + $hasfailed = true; + $params[$i] = false; + $reset = true; + $redo = true; + $failed = false; + PEAR_Downloader_Package::removeDuplicates($params); + continue 2; + } + } + } + PEAR::staticPopErrorHandling(); + if ($hasfailed && (isset($this->_options['ignore-errors']) || + isset($this->_options['nodeps']))) { + // this is probably not needed, but just in case + if (!isset($this->_options['soft'])) { + $this->log(0, 'WARNING: dependencies failed'); + } + } + } + + /** + * Retrieve the directory that downloads will happen in + * @access private + * @return string + */ + function getDownloadDir() + { + if (isset($this->_downloadDir)) { + return $this->_downloadDir; + } + $downloaddir = $this->config->get('download_dir'); + if (empty($downloaddir)) { + if (!class_exists('System')) { + require_once 'System.php'; + } + if (PEAR::isError($downloaddir = System::mktemp('-d'))) { + return $downloaddir; + } + $this->log(3, '+ tmp dir created at ' . $downloaddir); + } + if (!is_writable($downloaddir)) { + if (PEAR::isError(System::mkdir(array('-p', $downloaddir))) || + !is_writable($downloaddir)) { + return PEAR::raiseError('download directory "' . $downloaddir . + '" is not writeable. Change download_dir config variable to ' . + 'a writeable dir'); + } + } + return $this->_downloadDir = $downloaddir; + } + + function setDownloadDir($dir) + { + if (!@is_writable($dir)) { + if (PEAR::isError(System::mkdir(array('-p', $dir)))) { + return PEAR::raiseError('download directory "' . $dir . + '" is not writeable. Change download_dir config variable to ' . + 'a writeable dir'); + } + } + $this->_downloadDir = $dir; + } + + // }}} + // {{{ configSet() + function configSet($key, $value, $layer = 'user', $channel = false) + { + $this->config->set($key, $value, $layer, $channel); + $this->_preferredState = $this->config->get('preferred_state', null, $channel); + if (!$this->_preferredState) { + // don't inadvertantly use a non-set preferred_state + $this->_preferredState = null; + } + } + + // }}} + // {{{ setOptions() + function setOptions($options) + { + $this->_options = $options; + } + + // }}} + // {{{ setOptions() + function getOptions() + { + return $this->_options; + } + + // }}} + + /** + * For simpler unit-testing + * @param PEAR_Config + * @param int + * @param string + */ + function &getPackagefileObject(&$c, $d, $t = false) + { + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + $a = &new PEAR_PackageFile($c, $d, $t); + return $a; + } + + // {{{ _getPackageDownloadUrl() + + /** + * @param array output of {@link parsePackageName()} + * @access private + */ + function _getPackageDownloadUrl($parr) + { + $curchannel = $this->config->get('default_channel'); + $this->configSet('default_channel', $parr['channel']); + // getDownloadURL returns an array. On error, it only contains information + // on the latest release as array(version, info). On success it contains + // array(version, info, download url string) + $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state'); + if (!$this->_registry->channelExists($parr['channel'])) { + do { + if ($this->config->get('auto_discover')) { + if ($this->discover($parr['channel'])) { + break; + } + } + $this->configSet('default_channel', $curchannel); + return PEAR::raiseError('Unknown remote channel: ' . $remotechannel); + } while (false); + } + $chan = &$this->_registry->getChannel($parr['channel']); + if (PEAR::isError($chan)) { + return $chan; + } + $version = $this->_registry->packageInfo($parr['package'], 'version', + $parr['channel']); + $base2 = false; + if ($chan->supportsREST($this->config->get('preferred_mirror')) && + (($base2 = $chan->getBaseURL('REST1.3', $this->config->get('preferred_mirror'))) || + ($base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))))) { + if ($base2) { + $rest = &$this->config->getREST('1.3', $this->_options); + $base = $base2; + } else { + $rest = &$this->config->getREST('1.0', $this->_options); + } + if (!isset($parr['version']) && !isset($parr['state']) && $version + && !isset($this->_options['downloadonly'])) { + $url = $rest->getDownloadURL($base, $parr, $state, $version); + } else { + $url = $rest->getDownloadURL($base, $parr, $state, false); + } + if (PEAR::isError($url)) { + $this->configSet('default_channel', $curchannel); + return $url; + } + if ($parr['channel'] != $curchannel) { + $this->configSet('default_channel', $curchannel); + } + if (!is_array($url)) { + return $url; + } + $url['raw'] = false; // no checking is necessary for REST + if (!is_array($url['info'])) { + return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' . + 'this should never happen'); + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $testversion = $this->_registry->packageInfo($url['package'], 'version', + $parr['channel']); + PEAR::staticPopErrorHandling(); + if (!isset($this->_options['force']) && + !isset($this->_options['downloadonly']) && + !PEAR::isError($testversion) && + !isset($parr['group'])) { + if (version_compare($testversion, $url['version'], '>=')) { + return PEAR::raiseError($this->_registry->parsedPackageNameToString( + $parr, true) . ' is already installed and is newer than detected ' . + 'release version ' . $url['version'], -976); + } + } + if (isset($url['info']['required']) || $url['compatible']) { + require_once 'PEAR/PackageFile/v2.php'; + $pf = new PEAR_PackageFile_v2; + $pf->setRawChannel($parr['channel']); + if ($url['compatible']) { + $pf->setRawCompatible($url['compatible']); + } + } else { + require_once 'PEAR/PackageFile/v1.php'; + $pf = new PEAR_PackageFile_v1; + } + $pf->setRawPackage($url['package']); + $pf->setDeps($url['info']); + if ($url['compatible']) { + $pf->setCompatible($url['compatible']); + } + $pf->setRawState($url['stability']); + $url['info'] = &$pf; + if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { + $ext = '.tar'; + } else { + $ext = '.tgz'; + } + if (is_array($url)) { + if (isset($url['url'])) { + $url['url'] .= $ext; + } + } + return $url; + } elseif ($chan->supports('xmlrpc', 'package.getDownloadURL', false, '1.1')) { + // don't install with the old version information unless we're doing a plain + // vanilla simple installation. If the user says to install a particular + // version or state, ignore the current installed version + if (!isset($parr['version']) && !isset($parr['state']) && $version + && !isset($this->_options['downloadonly'])) { + $url = $this->_remote->call('package.getDownloadURL', $parr, $state, $version); + } else { + $url = $this->_remote->call('package.getDownloadURL', $parr, $state); + } + } else { + $url = $this->_remote->call('package.getDownloadURL', $parr, $state); + } + if (PEAR::isError($url)) { + return $url; + } + if ($parr['channel'] != $curchannel) { + $this->configSet('default_channel', $curchannel); + } + if (isset($url['__PEAR_ERROR_CLASS__'])) { + return PEAR::raiseError($url['message']); + } + if (!is_array($url)) { + return $url; + } + $url['raw'] = $url['info']; + if (isset($this->_options['downloadonly'])) { + $pkg = &$this->getPackagefileObject($this->config, $this->debug); + } else { + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (PEAR::isError($dir = $this->getDownloadDir())) { + PEAR::staticPopErrorHandling(); + return $dir; + } + PEAR::staticPopErrorHandling(); + $pkg = &$this->getPackagefileObject($this->config, $this->debug, $dir); + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pinfo = &$pkg->fromXmlString($url['info'], PEAR_VALIDATE_DOWNLOADING, 'remote'); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pinfo)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $pinfo->getMessage()); + } + return PEAR::raiseError('Remote package.xml is not valid - this should never happen'); + } + $url['info'] = &$pinfo; + if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { + $ext = '.tar'; + } else { + $ext = '.tgz'; + } + if (is_array($url)) { + if (isset($url['url'])) { + $url['url'] .= $ext; + } + } + return $url; + } + // }}} + // {{{ getDepPackageDownloadUrl() + + /** + * @param array dependency array + * @access private + */ + function _getDepPackageDownloadUrl($dep, $parr) + { + $xsdversion = isset($dep['rel']) ? '1.0' : '2.0'; + $curchannel = $this->config->get('default_channel'); + if (isset($dep['uri'])) { + $xsdversion = '2.0'; + $chan = &$this->_registry->getChannel('__uri'); + if (PEAR::isError($chan)) { + return $chan; + } + $version = $this->_registry->packageInfo($dep['name'], 'version', '__uri'); + $this->configSet('default_channel', '__uri'); + } else { + if (isset($dep['channel'])) { + $remotechannel = $dep['channel']; + } else { + $remotechannel = 'pear.php.net'; + } + if (!$this->_registry->channelExists($remotechannel)) { + do { + if ($this->config->get('auto_discover')) { + if ($this->discover($remotechannel)) { + break; + } + } + return PEAR::raiseError('Unknown remote channel: ' . $remotechannel); + } while (false); + } + $chan = &$this->_registry->getChannel($remotechannel); + if (PEAR::isError($chan)) { + return $chan; + } + $version = $this->_registry->packageInfo($dep['name'], 'version', + $remotechannel); + $this->configSet('default_channel', $remotechannel); + } + $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state'); + if (isset($parr['state']) && isset($parr['version'])) { + unset($parr['state']); + } + if (isset($dep['uri'])) { + $info = &$this->newDownloaderPackage($this); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $err = $info->initialize($dep); + PEAR::staticPopErrorHandling(); + if (!$err) { + // skip parameters that were missed by preferred_state + return PEAR::raiseError('Cannot initialize dependency'); + } + if (PEAR::isError($err)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $err->getMessage()); + } + if (is_object($info)) { + $param = $info->getChannel() . '/' . $info->getPackage(); + } + return PEAR::raiseError('Package "' . $param . '" is not valid'); + } + return $info; + } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) && + $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) { + $rest = &$this->config->getREST('1.0', $this->_options); + $url = $rest->getDepDownloadURL($base, $xsdversion, $dep, $parr, + $state, $version); + if (PEAR::isError($url)) { + return $url; + } + if ($parr['channel'] != $curchannel) { + $this->configSet('default_channel', $curchannel); + } + if (!is_array($url)) { + return $url; + } + $url['raw'] = false; // no checking is necessary for REST + if (!is_array($url['info'])) { + return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' . + 'this should never happen'); + } + if (isset($url['info']['required'])) { + if (!class_exists('PEAR_PackageFile_v2')) { + require_once 'PEAR/PackageFile/v2.php'; + } + $pf = new PEAR_PackageFile_v2; + $pf->setRawChannel($remotechannel); + } else { + if (!class_exists('PEAR_PackageFile_v1')) { + require_once 'PEAR/PackageFile/v1.php'; + } + $pf = new PEAR_PackageFile_v1; + } + $pf->setRawPackage($url['package']); + $pf->setDeps($url['info']); + if ($url['compatible']) { + $pf->setCompatible($url['compatible']); + } + $pf->setRawState($url['stability']); + $url['info'] = &$pf; + if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { + $ext = '.tar'; + } else { + $ext = '.tgz'; + } + if (is_array($url)) { + if (isset($url['url'])) { + $url['url'] .= $ext; + } + } + return $url; + } elseif ($chan->supports('xmlrpc', 'package.getDepDownloadURL', false, '1.1')) { + if ($version) { + $url = $this->_remote->call('package.getDepDownloadURL', $xsdversion, $dep, $parr, + $state, $version); + } else { + $url = $this->_remote->call('package.getDepDownloadURL', $xsdversion, $dep, $parr, + $state); + } + } else { + $url = $this->_remote->call('package.getDepDownloadURL', $xsdversion, $dep, $parr, $state); + } + if ($this->config->get('default_channel') != $curchannel) { + $this->configSet('default_channel', $curchannel); + } + if (!is_array($url)) { + return $url; + } + if (isset($url['__PEAR_ERROR_CLASS__'])) { + return PEAR::raiseError($url['message']); + } + $url['raw'] = $url['info']; + $pkg = &$this->getPackagefileObject($this->config, $this->debug); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pinfo = &$pkg->fromXmlString($url['info'], PEAR_VALIDATE_DOWNLOADING, 'remote'); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pinfo)) { + if (!isset($this->_options['soft'])) { + $this->log(0, $pinfo->getMessage()); + } + return PEAR::raiseError('Remote package.xml is not valid - this should never happen'); + } + $url['info'] = &$pinfo; + if (is_array($url)) { + if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { + $ext = '.tar'; + } else { + $ext = '.tgz'; + } + if (isset($url['url'])) { + $url['url'] .= $ext; + } + } + return $url; + } + // }}} + // {{{ getPackageDownloadUrl() + + /** + * @deprecated in favor of _getPackageDownloadUrl + */ + function getPackageDownloadUrl($package, $version = null, $channel = false) + { + if ($version) { + $package .= "-$version"; + } + if ($this === null || $this->_registry === null) { + $package = "http://pear.php.net/get/$package"; + } else { + $chan = $this->_registry->getChannel($channel); + if (PEAR::isError($chan)) { + return ''; + } + $package = "http://" . $chan->getServer() . "/get/$package"; + } + if (!extension_loaded("zlib")) { + $package .= '?uncompress=yes'; + } + return $package; + } + + // }}} + // {{{ getDownloadedPackages() + + /** + * Retrieve a list of downloaded packages after a call to {@link download()}. + * + * Also resets the list of downloaded packages. + * @return array + */ + function getDownloadedPackages() + { + $ret = $this->_downloadedPackages; + $this->_downloadedPackages = array(); + $this->_toDownload = array(); + return $ret; + } + + // }}} + // {{{ _downloadCallback() + + function _downloadCallback($msg, $params = null) + { + switch ($msg) { + case 'saveas': + $this->log(1, "downloading $params ..."); + break; + case 'done': + $this->log(1, '...done: ' . number_format($params, 0, '', ',') . ' bytes'); + break; + case 'bytesread': + static $bytes; + if (empty($bytes)) { + $bytes = 0; + } + if (!($bytes % 10240)) { + $this->log(1, '.', false); + } + $bytes += $params; + break; + case 'start': + if($params[1] == -1) { + $length = "Unknown size"; + } else { + $length = number_format($params[1], 0, '', ',')." bytes"; + } + $this->log(1, "Starting to download {$params[0]} ($length)"); + break; + } + if (method_exists($this->ui, '_downloadCallback')) + $this->ui->_downloadCallback($msg, $params); + } + + // }}} + // {{{ _prependPath($path, $prepend) + + function _prependPath($path, $prepend) + { + if (strlen($prepend) > 0) { + if (OS_WINDOWS && preg_match('/^[a-z]:/i', $path)) { + if (preg_match('/^[a-z]:/i', $prepend)) { + $prepend = substr($prepend, 2); + } elseif ($prepend{0} != '\\') { + $prepend = "\\$prepend"; + } + $path = substr($path, 0, 2) . $prepend . substr($path, 2); + } else { + $path = $prepend . $path; + } + } + return $path; + } + // }}} + // {{{ pushError($errmsg, $code) + + /** + * @param string + * @param integer + */ + function pushError($errmsg, $code = -1) + { + array_push($this->_errorStack, array($errmsg, $code)); + } + + // }}} + // {{{ getErrorMsgs() + + function getErrorMsgs() + { + $msgs = array(); + $errs = $this->_errorStack; + foreach ($errs as $err) { + $msgs[] = $err[0]; + } + $this->_errorStack = array(); + return $msgs; + } + + // }}} + + /** + * for BC + */ + function sortPkgDeps(&$packages, $uninstall = false) + { + $uninstall ? + $this->sortPackagesForUninstall($packages) : + $this->sortPackagesForInstall($packages); + } + + /** + * Sort a list of arrays of array(downloaded packagefilename) by dependency. + * + * This uses the topological sort method from graph theory, and the + * Structures_Graph package to properly sort dependencies for installation. + * @param array an array of downloaded PEAR_Downloader_Packages + * @return array array of array(packagefilename, package.xml contents) + */ + function sortPackagesForInstall(&$packages) + { + require_once 'Structures/Graph.php'; + require_once 'Structures/Graph/Node.php'; + require_once 'Structures/Graph/Manipulator/TopologicalSorter.php'; + $depgraph = new Structures_Graph(true); + $nodes = array(); + $reg = &$this->config->getRegistry(); + foreach ($packages as $i => $package) { + $pname = $reg->parsedPackageNameToString( + array( + 'channel' => $package->getChannel(), + 'package' => strtolower($package->getPackage()), + )); + $nodes[$pname] = new Structures_Graph_Node; + $nodes[$pname]->setData($packages[$i]); + $depgraph->addNode($nodes[$pname]); + } + $deplinks = array(); + foreach ($nodes as $package => $node) { + $pf = &$node->getData(); + $pdeps = $pf->getDeps(true); + if (!$pdeps) { + continue; + } + if ($pf->getPackagexmlVersion() == '1.0') { + foreach ($pdeps as $dep) { + if ($dep['type'] != 'pkg' || + (isset($dep['optional']) && $dep['optional'] == 'yes')) { + continue; + } + $dname = $reg->parsedPackageNameToString( + array( + 'channel' => 'pear.php.net', + 'package' => strtolower($dep['name']), + )); + if (isset($nodes[$dname])) + { + if (!isset($deplinks[$dname])) { + $deplinks[$dname] = array(); + } + $deplinks[$dname][$package] = 1; + // dependency is in installed packages + continue; + } + $dname = $reg->parsedPackageNameToString( + array( + 'channel' => 'pecl.php.net', + 'package' => strtolower($dep['name']), + )); + if (isset($nodes[$dname])) + { + if (!isset($deplinks[$dname])) { + $deplinks[$dname] = array(); + } + $deplinks[$dname][$package] = 1; + // dependency is in installed packages + continue; + } + } + } else { + // the only ordering we care about is: + // 1) subpackages must be installed before packages that depend on them + // 2) required deps must be installed before packages that depend on them + if (isset($pdeps['required']['subpackage'])) { + $t = $pdeps['required']['subpackage']; + if (!isset($t[0])) { + $t = array($t); + } + $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); + } + if (isset($pdeps['group'])) { + if (!isset($pdeps['group'][0])) { + $pdeps['group'] = array($pdeps['group']); + } + foreach ($pdeps['group'] as $group) { + if (isset($group['subpackage'])) { + $t = $group['subpackage']; + if (!isset($t[0])) { + $t = array($t); + } + $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); + } + } + } + if (isset($pdeps['optional']['subpackage'])) { + $t = $pdeps['optional']['subpackage']; + if (!isset($t[0])) { + $t = array($t); + } + $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); + } + if (isset($pdeps['required']['package'])) { + $t = $pdeps['required']['package']; + if (!isset($t[0])) { + $t = array($t); + } + $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); + } + if (isset($pdeps['group'])) { + if (!isset($pdeps['group'][0])) { + $pdeps['group'] = array($pdeps['group']); + } + foreach ($pdeps['group'] as $group) { + if (isset($group['package'])) { + $t = $group['package']; + if (!isset($t[0])) { + $t = array($t); + } + $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); + } + } + } + } + } + $this->_detectDepCycle($deplinks); + foreach ($deplinks as $dependent => $parents) { + foreach ($parents as $parent => $unused) { + $nodes[$dependent]->connectTo($nodes[$parent]); + } + } + $installOrder = Structures_Graph_Manipulator_TopologicalSorter::sort($depgraph); + $ret = array(); + for ($i = 0; $i < count($installOrder); $i++) { + foreach ($installOrder[$i] as $index => $sortedpackage) { + $data = &$installOrder[$i][$index]->getData(); + $ret[] = &$nodes[$reg->parsedPackageNameToString( + array( + 'channel' => $data->getChannel(), + 'package' => strtolower($data->getPackage()), + ))]->getData(); + } + } + $packages = $ret; + return; + } + + /** + * Detect recursive links between dependencies and break the cycles + * + * @param array + * @access private + */ + function _detectDepCycle(&$deplinks) + { + do { + $keepgoing = false; + foreach ($deplinks as $dep => $parents) { + foreach ($parents as $parent => $unused) { + // reset the parent cycle detector + $this->_testCycle(null, null, null); + if ($this->_testCycle($dep, $deplinks, $parent)) { + $keepgoing = true; + unset($deplinks[$dep][$parent]); + if (count($deplinks[$dep]) == 0) { + unset($deplinks[$dep]); + } + continue 3; + } + } + } + } while ($keepgoing); + } + + function _testCycle($test, $deplinks, $dep) + { + static $visited = array(); + if ($test === null) { + $visited = array(); + return; + } + // this happens when a parent has a dep cycle on another dependency + // but the child is not part of the cycle + if (isset($visited[$dep])) { + return false; + } + $visited[$dep] = 1; + if ($test == $dep) { + return true; + } + if (isset($deplinks[$dep])) { + if (in_array($test, array_keys($deplinks[$dep]), true)) { + return true; + } + foreach ($deplinks[$dep] as $parent => $unused) { + if ($this->_testCycle($test, $deplinks, $parent)) { + return true; + } + } + } + return false; + } + + /** + * Set up the dependency for installation parsing + * + * @param array $t dependency information + * @param PEAR_Registry $reg + * @param array $deplinks list of dependency links already established + * @param array $nodes all existing package nodes + * @param string $package parent package name + * @access private + */ + function _setupGraph($t, $reg, &$deplinks, &$nodes, $package) + { + foreach ($t as $dep) { + $depchannel = !isset($dep['channel']) ? + '__uri': $dep['channel']; + $dname = $reg->parsedPackageNameToString( + array( + 'channel' => $depchannel, + 'package' => strtolower($dep['name']), + )); + if (isset($nodes[$dname])) + { + if (!isset($deplinks[$dname])) { + $deplinks[$dname] = array(); + } + $deplinks[$dname][$package] = 1; + } + } + } + + function _dependsOn($a, $b) + { + return $this->_checkDepTree(strtolower($a->getChannel()), strtolower($a->getPackage()), + $b); + } + + function _checkDepTree($channel, $package, $b, $checked = array()) + { + $checked[$channel][$package] = true; + if (!isset($this->_depTree[$channel][$package])) { + return false; + } + if (isset($this->_depTree[$channel][$package][strtolower($b->getChannel())] + [strtolower($b->getPackage())])) { + return true; + } + foreach ($this->_depTree[$channel][$package] as $ch => $packages) { + foreach ($packages as $pa => $true) { + if ($this->_checkDepTree($ch, $pa, $b, $checked)) { + return true; + } + } + } + return false; + } + + function _sortInstall($a, $b) + { + if (!$a->getDeps() && !$b->getDeps()) { + return 0; // neither package has dependencies, order is insignificant + } + if ($a->getDeps() && !$b->getDeps()) { + return 1; // $a must be installed after $b because $a has dependencies + } + if (!$a->getDeps() && $b->getDeps()) { + return -1; // $b must be installed after $a because $b has dependencies + } + // both packages have dependencies + if ($this->_dependsOn($a, $b)) { + return 1; + } + if ($this->_dependsOn($b, $a)) { + return -1; + } + return 0; + } + + /** + * Download a file through HTTP. Considers suggested file name in + * Content-disposition: header and can run a callback function for + * different events. The callback will be called with two + * parameters: the callback type, and parameters. The implemented + * callback types are: + * + * 'setup' called at the very beginning, parameter is a UI object + * that should be used for all output + * 'message' the parameter is a string with an informational message + * 'saveas' may be used to save with a different file name, the + * parameter is the filename that is about to be used. + * If a 'saveas' callback returns a non-empty string, + * that file name will be used as the filename instead. + * Note that $save_dir will not be affected by this, only + * the basename of the file. + * 'start' download is starting, parameter is number of bytes + * that are expected, or -1 if unknown + * 'bytesread' parameter is the number of bytes read so far + * 'done' download is complete, parameter is the total number + * of bytes read + * 'connfailed' if the TCP/SSL connection fails, this callback is called + * with array(host,port,errno,errmsg) + * 'writefailed' if writing to disk fails, this callback is called + * with array(destfile,errmsg) + * + * If an HTTP proxy has been configured (http_proxy PEAR_Config + * setting), the proxy will be used. + * + * @param string $url the URL to download + * @param object $ui PEAR_Frontend_* instance + * @param object $config PEAR_Config instance + * @param string $save_dir directory to save file in + * @param mixed $callback function/method to call for status + * updates + * @param false|string|array $lastmodified header values to check against for caching + * use false to return the header values from this download + * @param false|array $accept Accept headers to send + * @return string|array Returns the full path of the downloaded file or a PEAR + * error on failure. If the error is caused by + * socket-related errors, the error object will + * have the fsockopen error code available through + * getCode(). If caching is requested, then return the header + * values. + * + * @access public + */ + function downloadHttp($url, &$ui, $save_dir = '.', $callback = null, $lastmodified = null, + $accept = false) + { + static $redirect = 0; + // allways reset , so we are clean case of error + $wasredirect = $redirect; + $redirect = 0; + if ($callback) { + call_user_func($callback, 'setup', array(&$ui)); + } + $info = parse_url($url); + if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) { + return PEAR::raiseError('Cannot download non-http URL "' . $url . '"'); + } + if (!isset($info['host'])) { + return PEAR::raiseError('Cannot download from non-URL "' . $url . '"'); + } else { + $host = isset($info['host']) ? $info['host'] : null; + $port = isset($info['port']) ? $info['port'] : null; + $path = isset($info['path']) ? $info['path'] : null; + } + if (isset($this)) { + $config = &$this->config; + } else { + $config = &PEAR_Config::singleton(); + } + $proxy_host = $proxy_port = $proxy_user = $proxy_pass = ''; + if ($config->get('http_proxy') && + $proxy = parse_url($config->get('http_proxy'))) { + $proxy_host = isset($proxy['host']) ? $proxy['host'] : null; + if (isset($proxy['scheme']) && $proxy['scheme'] == 'https') { + $proxy_host = 'ssl://' . $proxy_host; + } + $proxy_port = isset($proxy['port']) ? $proxy['port'] : 8080; + $proxy_user = isset($proxy['user']) ? urldecode($proxy['user']) : null; + $proxy_pass = isset($proxy['pass']) ? urldecode($proxy['pass']) : null; + + if ($callback) { + call_user_func($callback, 'message', "Using HTTP proxy $host:$port"); + } + } + if (empty($port)) { + if (isset($info['scheme']) && $info['scheme'] == 'https') { + $port = 443; + } else { + $port = 80; + } + } + if ($proxy_host != '') { + $fp = @fsockopen($proxy_host, $proxy_port, $errno, $errstr); + if (!$fp) { + if ($callback) { + call_user_func($callback, 'connfailed', array($proxy_host, $proxy_port, + $errno, $errstr)); + } + return PEAR::raiseError("Connection to `$proxy_host:$proxy_port' failed: $errstr", $errno); + } + if ($lastmodified === false || $lastmodified) { + $request = "GET $url HTTP/1.1\r\n"; + } else { + $request = "GET $url HTTP/1.0\r\n"; + } + } else { + if (isset($info['scheme']) && $info['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + $fp = @fsockopen($host, $port, $errno, $errstr); + if (!$fp) { + if ($callback) { + call_user_func($callback, 'connfailed', array($host, $port, + $errno, $errstr)); + } + return PEAR::raiseError("Connection to `$host:$port' failed: $errstr", $errno); + } + if ($lastmodified === false || $lastmodified) { + $request = "GET $path HTTP/1.1\r\n"; + $request .= "Host: $host:$port\r\n"; + } else { + $request = "GET $path HTTP/1.0\r\n"; + $request .= "Host: $host\r\n"; + } + } + $ifmodifiedsince = ''; + if (is_array($lastmodified)) { + if (isset($lastmodified['Last-Modified'])) { + $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n"; + } + if (isset($lastmodified['ETag'])) { + $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n"; + } + } else { + $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : ''); + } + $request .= $ifmodifiedsince . "User-Agent: PEAR/1.6.1/PHP/" . + PHP_VERSION . "\r\n"; + if (isset($this)) { // only pass in authentication for non-static calls + $username = $config->get('username'); + $password = $config->get('password'); + if ($username && $password) { + $tmp = base64_encode("$username:$password"); + $request .= "Authorization: Basic $tmp\r\n"; + } + } + if ($proxy_host != '' && $proxy_user != '') { + $request .= 'Proxy-Authorization: Basic ' . + base64_encode($proxy_user . ':' . $proxy_pass) . "\r\n"; + } + if ($accept) { + $request .= 'Accept: ' . implode(', ', $accept) . "\r\n"; + } + $request .= "Connection: close\r\n"; + $request .= "\r\n"; + fwrite($fp, $request); + $headers = array(); + $reply = 0; + while (trim($line = fgets($fp, 1024))) { + if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) { + $headers[strtolower($matches[1])] = trim($matches[2]); + } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) { + $reply = (int) $matches[1]; + if ($reply == 304 && ($lastmodified || ($lastmodified === false))) { + return false; + } + if (! in_array($reply, array(200, 301, 302, 303, 305, 307))) { + return PEAR::raiseError("File http://$host:$port$path not valid (received: $line)"); + } + } + } + if ($reply != 200) { + if (isset($headers['location'])) { + if ($wasredirect < 5) { + $redirect = $wasredirect + 1; + return $this->downloadHttp($headers['location'], + $ui, $save_dir, $callback, $lastmodified, $accept); + } else { + return PEAR::raiseError("File http://$host:$port$path not valid (redirection looped more than 5 times)"); + } + } else { + return PEAR::raiseError("File http://$host:$port$path not valid (redirected but no location)"); + } + } + if (isset($headers['content-disposition']) && + preg_match('/\sfilename=\"([^;]*\S)\"\s*(;|\\z)/', $headers['content-disposition'], $matches)) { + $save_as = basename($matches[1]); + } else { + $save_as = basename($url); + } + if ($callback) { + $tmp = call_user_func($callback, 'saveas', $save_as); + if ($tmp) { + $save_as = $tmp; + } + } + $dest_file = $save_dir . DIRECTORY_SEPARATOR . $save_as; + if (!$wp = @fopen($dest_file, 'wb')) { + fclose($fp); + if ($callback) { + call_user_func($callback, 'writefailed', array($dest_file, $php_errormsg)); + } + return PEAR::raiseError("could not open $dest_file for writing"); + } + if (isset($headers['content-length'])) { + $length = $headers['content-length']; + } else { + $length = -1; + } + $bytes = 0; + if ($callback) { + call_user_func($callback, 'start', array(basename($dest_file), $length)); + } + while ($data = fread($fp, 1024)) { + $bytes += strlen($data); + if ($callback) { + call_user_func($callback, 'bytesread', $bytes); + } + if (!@fwrite($wp, $data)) { + fclose($fp); + if ($callback) { + call_user_func($callback, 'writefailed', array($dest_file, $php_errormsg)); + } + return PEAR::raiseError("$dest_file: write failed ($php_errormsg)"); + } + } + fclose($fp); + fclose($wp); + if ($callback) { + call_user_func($callback, 'done', $bytes); + } + if ($lastmodified === false || $lastmodified) { + if (isset($headers['etag'])) { + $lastmodified = array('ETag' => $headers['etag']); + } + if (isset($headers['last-modified'])) { + if (is_array($lastmodified)) { + $lastmodified['Last-Modified'] = $headers['last-modified']; + } else { + $lastmodified = $headers['last-modified']; + } + } + return array($dest_file, $lastmodified, $headers); + } + return $dest_file; + } +} +// }}} + +?> diff --git a/PEAR/Downloader/Package.php b/PEAR/Downloader/Package.php new file mode 100644 index 0000000..07a9ebf --- /dev/null +++ b/PEAR/Downloader/Package.php @@ -0,0 +1,1851 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Package.php,v 1.110 2007/05/31 03:51:08 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Error code when parameter initialization fails because no releases + * exist within preferred_state, but releases do exist + */ +define('PEAR_DOWNLOADER_PACKAGE_STATE', -1003); +/** + * Error code when parameter initialization fails because no releases + * exist that will work with the existing PHP version + */ +define('PEAR_DOWNLOADER_PACKAGE_PHPVERSION', -1004); +/** + * Coordinates download parameters and manages their dependencies + * prior to downloading them. + * + * Input can come from three sources: + * + * - local files (archives or package.xml) + * - remote files (downloadable urls) + * - abstract package names + * + * The first two elements are handled cleanly by PEAR_PackageFile, but the third requires + * accessing pearweb's xml-rpc interface to determine necessary dependencies, and the + * format returned of dependencies is slightly different from that used in package.xml. + * + * This class hides the differences between these elements, and makes automatic + * dependency resolution a piece of cake. It also manages conflicts when + * two classes depend on incompatible dependencies, or differing versions of the same + * package dependency. In addition, download will not be attempted if the php version is + * not supported, PEAR installer version is not supported, or non-PECL extensions are not + * installed. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Downloader_Package +{ + /** + * @var PEAR_Downloader + */ + var $_downloader; + /** + * @var PEAR_Config + */ + var $_config; + /** + * @var PEAR_Registry + */ + var $_registry; + /** + * Used to implement packagingroot properly + * @var PEAR_Registry + */ + var $_installRegistry; + /** + * @var PEAR_PackageFile_v1|PEAR_PackageFile|v2 + */ + var $_packagefile; + /** + * @var array + */ + var $_parsedname; + /** + * @var array + */ + var $_downloadURL; + /** + * @var array + */ + var $_downloadDeps = array(); + /** + * @var boolean + */ + var $_valid = false; + /** + * @var boolean + */ + var $_analyzed = false; + /** + * if this or a parent package was invoked with Package-state, this is set to the + * state variable. + * + * This allows temporary reassignment of preferred_state for a parent package and all of + * its dependencies. + * @var string|false + */ + var $_explicitState = false; + /** + * If this package is invoked with Package#group, this variable will be true + */ + var $_explicitGroup = false; + /** + * Package type local|url|xmlrpc + * @var string + */ + var $_type; + /** + * Contents of package.xml, if downloaded from a remote channel + * @var string|false + * @access private + */ + var $_rawpackagefile; + /** + * @var boolean + * @access private + */ + var $_validated = false; + + /** + * @param PEAR_Downloader + */ + function PEAR_Downloader_Package(&$downloader) + { + $this->_downloader = &$downloader; + $this->_config = &$this->_downloader->config; + $this->_registry = &$this->_config->getRegistry(); + $options = $downloader->getOptions(); + if (isset($options['packagingroot'])) { + $this->_config->setInstallRoot($options['packagingroot']); + $this->_installRegistry = &$this->_config->getRegistry(); + $this->_config->setInstallRoot(false); + } else { + $this->_installRegistry = &$this->_registry; + } + $this->_valid = $this->_analyzed = false; + } + + /** + * Parse the input and determine whether this is a local file, a remote uri, or an + * abstract package name. + * + * This is the heart of the PEAR_Downloader_Package(), and is used in + * {@link PEAR_Downloader::download()} + * @param string + * @return bool|PEAR_Error + */ + function initialize($param) + { + $origErr = $this->_fromFile($param); + if (!$this->_valid) { + $options = $this->_downloader->getOptions(); + if (isset($options['offline'])) { + if (PEAR::isError($origErr)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $origErr->getMessage()); + } + } + return PEAR::raiseError('Cannot download non-local package "' . $param . '"'); + } + $err = $this->_fromUrl($param); + if (PEAR::isError($err) || !$this->_valid) { + if ($this->_type == 'url') { + if (PEAR::isError($err)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $err->getMessage()); + } + } + return PEAR::raiseError("Invalid or missing remote package file"); + } + $err = $this->_fromString($param); + if (PEAR::isError($err) || !$this->_valid) { + if (PEAR::isError($err) && + $err->getCode() == PEAR_DOWNLOADER_PACKAGE_STATE) { + return false; // instruct the downloader to silently skip + } + if (isset($this->_type) && $this->_type == 'local' && + PEAR::isError($origErr)) { + if (is_array($origErr->getUserInfo())) { + foreach ($origErr->getUserInfo() as $err) { + if (is_array($err)) { + $err = $err['message']; + } + if (!isset($options['soft'])) { + $this->_downloader->log(0, $err); + } + } + } + if (!isset($options['soft'])) { + $this->_downloader->log(0, $origErr->getMessage()); + } + if (is_array($param)) { + $param = $this->_registry->parsedPackageNameToString($param, + true); + } + return PEAR::raiseError( + "Cannot initialize '$param', invalid or missing package file"); + } + if (PEAR::isError($err)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $err->getMessage()); + } + } + if (is_array($param)) { + $param = $this->_registry->parsedPackageNameToString($param, true); + } + return PEAR::raiseError( + "Cannot initialize '$param', invalid or missing package file"); + } + } + } + return true; + } + + /** + * Retrieve any non-local packages + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2|PEAR_Error + */ + function &download() + { + if (isset($this->_packagefile)) { + return $this->_packagefile; + } + if (isset($this->_downloadURL['url'])) { + $this->_isvalid = false; + $info = $this->getParsedPackage(); + foreach ($info as $i => $p) { + $info[$i] = strtolower($p); + } + $err = $this->_fromUrl($this->_downloadURL['url'], + $this->_registry->parsedPackageNameToString($this->_parsedname, true)); + $newinfo = $this->getParsedPackage(); + foreach ($newinfo as $i => $p) { + $newinfo[$i] = strtolower($p); + } + if ($info != $newinfo) { + do { + if ($info['package'] == 'pecl.php.net' && $newinfo['package'] == 'pear.php.net') { + $info['package'] = 'pear.php.net'; + if ($info == $newinfo) { + // skip the channel check if a pecl package says it's a PEAR package + break; + } + } + return PEAR::raiseError('CRITICAL ERROR: We are ' . + $this->_registry->parsedPackageNameToString($info) . ', but the file ' . + 'downloaded claims to be ' . + $this->_registry->parsedPackageNameToString($this->getParsedPackage())); + } while (false); + } + if (PEAR::isError($err) || !$this->_valid) { + return $err; + } + } + $this->_type = 'local'; + return $this->_packagefile; + } + + function &getPackageFile() + { + return $this->_packagefile; + } + + function &getDownloader() + { + return $this->_downloader; + } + + function getType() + { + return $this->_type; + } + + /** + * Like {@link initialize()}, but operates on a dependency + */ + function fromDepURL($dep) + { + $this->_downloadURL = $dep; + if (isset($dep['uri'])) { + $options = $this->_downloader->getOptions(); + if (!extension_loaded("zlib") || isset($options['nocompress'])) { + $ext = '.tar'; + } else { + $ext = '.tgz'; + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->_fromUrl($dep['uri'] . $ext); + PEAR::popErrorHandling(); + if (PEAR::isError($err)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $err->getMessage()); + } + return PEAR::raiseError('Invalid uri dependency "' . $dep['uri'] . $ext . '", ' . + 'cannot download'); + } + } else { + $this->_parsedname = + array( + 'package' => $dep['info']->getPackage(), + 'channel' => $dep['info']->getChannel(), + 'version' => $dep['version'] + ); + if (!isset($dep['nodefault'])) { + $this->_parsedname['group'] = 'default'; // download the default dependency group + $this->_explicitGroup = false; + } + $this->_rawpackagefile = $dep['raw']; + } + } + + function detectDependencies($params) + { + $options = $this->_downloader->getOptions(); + if (isset($options['downloadonly'])) { + return; + } + if (isset($options['offline'])) { + $this->_downloader->log(3, 'Skipping dependency download check, --offline specified'); + return; + } + $pname = $this->getParsedPackage(); + if (!$pname) { + return; + } + $deps = $this->getDeps(); + if (!$deps) { + return; + } + if (isset($deps['required'])) { // package.xml 2.0 + return $this->_detect2($deps, $pname, $options, $params); + } else { + return $this->_detect1($deps, $pname, $options, $params); + } + } + + function setValidated() + { + $this->_validated = true; + } + + function alreadyValidated() + { + return $this->_validated; + } + + /** + * Remove packages to be downloaded that are already installed + * @param array of PEAR_Downloader_Package objects + * @static + */ + function removeInstalled(&$params) + { + if (!isset($params[0])) { + return; + } + $options = $params[0]->_downloader->getOptions(); + if (!isset($options['downloadonly'])) { + foreach ($params as $i => $param) { + // remove self if already installed with this version + // this does not need any pecl magic - we only remove exact matches + if ($param->_installRegistry->packageExists($param->getPackage(), $param->getChannel())) { + if (version_compare($param->_installRegistry->packageInfo($param->getPackage(), 'version', + $param->getChannel()), $param->getVersion(), '==')) { + if (!isset($options['force'])) { + $info = $param->getParsedPackage(); + unset($info['version']); + unset($info['state']); + if (!isset($options['soft'])) { + $param->_downloader->log(1, 'Skipping package "' . + $param->getShortName() . + '", already installed as version ' . + $param->_installRegistry->packageInfo($param->getPackage(), + 'version', $param->getChannel())); + } + $params[$i] = false; + } + } elseif (!isset($options['force']) && !isset($options['upgrade']) && + !isset($options['soft'])) { + $info = $param->getParsedPackage(); + $param->_downloader->log(1, 'Skipping package "' . + $param->getShortName() . + '", already installed as version ' . + $param->_installRegistry->packageInfo($param->getPackage(), 'version', + $param->getChannel())); + $params[$i] = false; + } + } + } + } + PEAR_Downloader_Package::removeDuplicates($params); + } + + function _detect2($deps, $pname, $options, $params) + { + $this->_downloadDeps = array(); + $groupnotfound = false; + foreach (array('package', 'subpackage') as $packagetype) { + // get required dependency group + if (isset($deps['required'][$packagetype])) { + if (isset($deps['required'][$packagetype][0])) { + foreach ($deps['required'][$packagetype] as $dep) { + if (isset($dep['conflicts'])) { + // skip any package that this package conflicts with + continue; + } + $ret = $this->_detect2Dep($dep, $pname, 'required', $params); + if (is_array($ret)) { + $this->_downloadDeps[] = $ret; + } + } + } else { + $dep = $deps['required'][$packagetype]; + if (!isset($dep['conflicts'])) { + // skip any package that this package conflicts with + $ret = $this->_detect2Dep($dep, $pname, 'required', $params); + if (is_array($ret)) { + $this->_downloadDeps[] = $ret; + } + } + } + } + // get optional dependency group, if any + if (isset($deps['optional'][$packagetype])) { + $skipnames = array(); + if (!isset($deps['optional'][$packagetype][0])) { + $deps['optional'][$packagetype] = array($deps['optional'][$packagetype]); + } + foreach ($deps['optional'][$packagetype] as $dep) { + $skip = false; + if (!isset($options['alldeps'])) { + $dep['package'] = $dep['name']; + if (!isset($options['soft'])) { + $this->_downloader->log(3, 'Notice: package "' . + $this->_registry->parsedPackageNameToString($this->getParsedPackage(), + true) . '" optional dependency "' . + $this->_registry->parsedPackageNameToString(array('package' => + $dep['name'], 'channel' => 'pear.php.net'), true) . + '" will not be automatically downloaded'); + } + $skipnames[] = $this->_registry->parsedPackageNameToString($dep, true); + $skip = true; + unset($dep['package']); + } + if (!($ret = $this->_detect2Dep($dep, $pname, 'optional', $params))) { + $dep['package'] = $dep['name']; + $skip = count($skipnames) ? + $skipnames[count($skipnames) - 1] : ''; + if ($skip == + $this->_registry->parsedPackageNameToString($dep, true)) { + array_pop($skipnames); + } + } + if (!$skip && is_array($ret)) { + $this->_downloadDeps[] = $ret; + } + } + if (count($skipnames)) { + if (!isset($options['soft'])) { + $this->_downloader->log(1, 'Did not download optional dependencies: ' . + implode(', ', $skipnames) . + ', use --alldeps to download automatically'); + } + } + } + // get requested dependency group, if any + $groupname = $this->getGroup(); + $explicit = $this->_explicitGroup; + if (!$groupname) { + if ($this->canDefault()) { + $groupname = 'default'; // try the default dependency group + } else { + continue; + } + } + if ($groupnotfound) { + continue; + } + if (isset($deps['group'])) { + if (isset($deps['group']['attribs'])) { + if (strtolower($deps['group']['attribs']['name']) == strtolower($groupname)) { + $group = $deps['group']; + } elseif ($explicit) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, 'Warning: package "' . + $this->_registry->parsedPackageNameToString($pname, true) . + '" has no dependency ' . 'group named "' . $groupname . '"'); + } + $groupnotfound = true; + continue; + } + } else { + $found = false; + foreach ($deps['group'] as $group) { + if (strtolower($group['attribs']['name']) == strtolower($groupname)) { + $found = true; + break; + } + } + if (!$found) { + if ($explicit) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, 'Warning: package "' . + $this->_registry->parsedPackageNameToString($pname, true) . + '" has no dependency ' . 'group named "' . $groupname . '"'); + } + } + $groupnotfound = true; + continue; + } + } + } + if (isset($group)) { + if (isset($group[$packagetype])) { + if (isset($group[$packagetype][0])) { + foreach ($group[$packagetype] as $dep) { + $ret = $this->_detect2Dep($dep, $pname, 'dependency group "' . + $group['attribs']['name'] . '"', $params); + if (is_array($ret)) { + $this->_downloadDeps[] = $ret; + } + } + } else { + $ret = $this->_detect2Dep($group[$packagetype], $pname, + 'dependency group "' . + $group['attribs']['name'] . '"', $params); + if (is_array($ret)) { + $this->_downloadDeps[] = $ret; + } + } + } + } + } + } + + function _detect2Dep($dep, $pname, $group, $params) + { + if (isset($dep['conflicts'])) { + return true; + } + $options = $this->_downloader->getOptions(); + if (isset($dep['uri'])) { + return array('uri' => $dep['uri'], 'dep' => $dep);; + } + $testdep = $dep; + $testdep['package'] = $dep['name']; + if (PEAR_Downloader_Package::willDownload($testdep, $params)) { + $dep['package'] = $dep['name']; + if (!isset($options['soft'])) { + $this->_downloader->log(2, $this->getShortName() . ': Skipping ' . $group . + ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", will be installed'); + } + return false; + } + $options = $this->_downloader->getOptions(); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + if ($this->_explicitState) { + $pname['state'] = $this->_explicitState; + } + $url = + $this->_downloader->_getDepPackageDownloadUrl($dep, $pname); + if (PEAR::isError($url)) { + PEAR::popErrorHandling(); + return $url; + } + $dep['package'] = $dep['name']; + $ret = $this->_analyzeDownloadURL($url, 'dependency', $dep, $params, $group == 'optional' && + !isset($options['alldeps']), true); + PEAR::popErrorHandling(); + if (PEAR::isError($ret)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + return false; + } else { + // check to see if a dep is already installed and is the same or newer + if (!isset($dep['min']) && !isset($dep['max']) && !isset($dep['recommended'])) { + $oper = 'has'; + } else { + $oper = 'gt'; + } + // do not try to move this before getDepPackageDownloadURL + // we can't determine whether upgrade is necessary until we know what + // version would be downloaded + if (!isset($options['force']) && $this->isInstalled($ret, $oper)) { + $version = $this->_installRegistry->packageInfo($dep['name'], 'version', + $dep['channel']); + $dep['package'] = $dep['name']; + if (!isset($options['soft'])) { + $this->_downloader->log(3, $this->getShortName() . ': Skipping ' . $group . + ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '" version ' . $url['version'] . ', already installed as version ' . + $version); + } + return false; + } + } + if (isset($dep['nodefault'])) { + $ret['nodefault'] = true; + } + return $ret; + } + + function _detect1($deps, $pname, $options, $params) + { + $this->_downloadDeps = array(); + $skipnames = array(); + foreach ($deps as $dep) { + $nodownload = false; + if ($dep['type'] == 'pkg') { + $dep['channel'] = 'pear.php.net'; + $dep['package'] = $dep['name']; + switch ($dep['rel']) { + case 'not' : + continue 2; + case 'ge' : + case 'eq' : + case 'gt' : + case 'has' : + $group = (!isset($dep['optional']) || $dep['optional'] == 'no') ? + 'required' : + 'optional'; + if (PEAR_Downloader_Package::willDownload($dep, $params)) { + $this->_downloader->log(2, $this->getShortName() . ': Skipping ' . $group + . ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", will be installed'); + continue 2; + } + $fakedp = new PEAR_PackageFile_v1; + $fakedp->setPackage($dep['name']); + // skip internet check if we are not upgrading (bug #5810) + if (!isset($options['upgrade']) && $this->isInstalled( + $fakedp, $dep['rel'])) { + $this->_downloader->log(2, $this->getShortName() . ': Skipping ' . $group + . ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", is already installed'); + continue 2; + } + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + if ($this->_explicitState) { + $pname['state'] = $this->_explicitState; + } + $url = + $this->_downloader->_getDepPackageDownloadUrl($dep, $pname); + $chan = 'pear.php.net'; + if (PEAR::isError($url)) { + // check to see if this is a pecl package that has jumped + // from pear.php.net to pecl.php.net channel + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + $newdep = PEAR_Dependency2::normalizeDep($dep); + $newdep = $newdep[0]; + $newdep['channel'] = 'pecl.php.net'; + $chan = 'pecl.php.net'; + $url = + $this->_downloader->_getDepPackageDownloadUrl($newdep, $pname); + $obj = &$this->_installRegistry->getPackage($dep['name']); + if (PEAR::isError($url)) { + PEAR::popErrorHandling(); + if ($obj !== null && $this->isInstalled($obj, $dep['rel'])) { + $group = (!isset($dep['optional']) || $dep['optional'] == 'no') ? + 'required' : + 'optional'; + $dep['package'] = $dep['name']; + if (!isset($options['soft'])) { + $this->_downloader->log(3, $this->getShortName() . + ': Skipping ' . $group . ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", already installed as version ' . $obj->getVersion()); + } + $skip = count($skipnames) ? + $skipnames[count($skipnames) - 1] : ''; + if ($skip == + $this->_registry->parsedPackageNameToString($dep, true)) { + array_pop($skipnames); + } + continue; + } else { + if (isset($dep['optional']) && $dep['optional'] == 'yes') { + $this->_downloader->log(2, $this->getShortName() . + ': Skipping optional dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", no releases exist'); + continue; + } else { + return $url; + } + } + } + } + PEAR::popErrorHandling(); + if (!isset($options['alldeps'])) { + if (isset($dep['optional']) && $dep['optional'] == 'yes') { + if (!isset($options['soft'])) { + $this->_downloader->log(3, 'Notice: package "' . + $this->getShortName() . + '" optional dependency "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $chan, 'package' => + $dep['name']), true) . + '" will not be automatically downloaded'); + } + $skipnames[] = $this->_registry->parsedPackageNameToString( + array('channel' => $chan, 'package' => + $dep['name']), true); + $nodownload = true; + } + } + if (!isset($options['alldeps']) && !isset($options['onlyreqdeps'])) { + if (!isset($dep['optional']) || $dep['optional'] == 'no') { + if (!isset($options['soft'])) { + $this->_downloader->log(3, 'Notice: package "' . + $this->getShortName() . + '" required dependency "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $chan, 'package' => + $dep['name']), true) . + '" will not be automatically downloaded'); + } + $skipnames[] = $this->_registry->parsedPackageNameToString( + array('channel' => $chan, 'package' => + $dep['name']), true); + $nodownload = true; + } + } + // check to see if a dep is already installed + // do not try to move this before getDepPackageDownloadURL + // we can't determine whether upgrade is necessary until we know what + // version would be downloaded + if (!isset($options['force']) && $this->isInstalled( + $url, $dep['rel'])) { + $group = (!isset($dep['optional']) || $dep['optional'] == 'no') ? + 'required' : + 'optional'; + $dep['package'] = $dep['name']; + if (isset($newdep)) { + $version = $this->_installRegistry->packageInfo($newdep['name'], 'version', + $newdep['channel']); + } else { + $version = $this->_installRegistry->packageInfo($dep['name'], 'version'); + } + $dep['version'] = $url['version']; + if (!isset($options['soft'])) { + $this->_downloader->log(3, $this->getShortName() . ': Skipping ' . $group . + ' dependency "' . + $this->_registry->parsedPackageNameToString($dep, true) . + '", already installed as version ' . $version); + } + $skip = count($skipnames) ? + $skipnames[count($skipnames) - 1] : ''; + if ($skip == + $this->_registry->parsedPackageNameToString($dep, true)) { + array_pop($skipnames); + } + continue; + } + if ($nodownload) { + continue; + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + if (isset($newdep)) { + $dep = $newdep; + } + $dep['package'] = $dep['name']; + $ret = $this->_analyzeDownloadURL($url, 'dependency', $dep, $params, + isset($dep['optional']) && $dep['optional'] == 'yes' && + !isset($options['alldeps']), true); + PEAR::popErrorHandling(); + if (PEAR::isError($ret)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + continue; + } + $this->_downloadDeps[] = $ret; + } + } + if (count($skipnames)) { + if (!isset($options['soft'])) { + $this->_downloader->log(1, 'Did not download dependencies: ' . + implode(', ', $skipnames) . + ', use --alldeps or --onlyreqdeps to download automatically'); + } + } + } + + function setDownloadURL($pkg) + { + $this->_downloadURL = $pkg; + } + + /** + * Set the package.xml object for this downloaded package + * + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 $pkg + */ + function setPackageFile(&$pkg) + { + $this->_packagefile = &$pkg; + } + + function getShortName() + { + return $this->_registry->parsedPackageNameToString(array('channel' => $this->getChannel(), + 'package' => $this->getPackage()), true); + } + + function getParsedPackage() + { + if (isset($this->_packagefile) || isset($this->_parsedname)) { + return array('channel' => $this->getChannel(), + 'package' => $this->getPackage(), + 'version' => $this->getVersion()); + } + return false; + } + + function getDownloadURL() + { + return $this->_downloadURL; + } + + function canDefault() + { + if (isset($this->_downloadURL)) { + if (isset($this->_downloadURL['nodefault'])) { + return false; + } + } + return true; + } + + function getPackage() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackage(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getPackage(); + } else { + return false; + } + } + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + */ + function isSubpackage(&$pf) + { + if (isset($this->_packagefile)) { + return $this->_packagefile->isSubpackage($pf); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->isSubpackage($pf); + } else { + return false; + } + } + + function getPackageType() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackageType(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getPackageType(); + } else { + return false; + } + } + + function isBundle() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackageType() == 'bundle'; + } else { + return false; + } + } + + function getPackageXmlVersion() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackagexmlVersion(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getPackagexmlVersion(); + } else { + return '1.0'; + } + } + + function getChannel() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getChannel(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getChannel(); + } else { + return false; + } + } + + function getURI() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getURI(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getURI(); + } else { + return false; + } + } + + function getVersion() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getVersion(); + } elseif (isset($this->_downloadURL['version'])) { + return $this->_downloadURL['version']; + } else { + return false; + } + } + + function isCompatible($pf) + { + if (isset($this->_packagefile)) { + return $this->_packagefile->isCompatible($pf); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->isCompatible($pf); + } else { + return true; + } + } + + function setGroup($group) + { + $this->_parsedname['group'] = $group; + } + + function getGroup() + { + if (isset($this->_parsedname['group'])) { + return $this->_parsedname['group']; + } else { + return ''; + } + } + + function isExtension($name) + { + if (isset($this->_packagefile)) { + return $this->_packagefile->isExtension($name); + } elseif (isset($this->_downloadURL['info'])) { + if ($this->_downloadURL['info']->getPackagexmlVersion() == '2.0') { + return $this->_downloadURL['info']->getProvidesExtension() == $name; + } else { + return false; + } + } else { + return false; + } + } + + function getDeps() + { + if (isset($this->_packagefile)) { + $ver = $this->_packagefile->getPackagexmlVersion(); + if (version_compare($ver, '2.0', '>=')) { + return $this->_packagefile->getDeps(true); + } else { + return $this->_packagefile->getDeps(); + } + } elseif (isset($this->_downloadURL['info'])) { + $ver = $this->_downloadURL['info']->getPackagexmlVersion(); + if (version_compare($ver, '2.0', '>=')) { + return $this->_downloadURL['info']->getDeps(true); + } else { + return $this->_downloadURL['info']->getDeps(); + } + } else { + return array(); + } + } + + /** + * @param array Parsed array from {@link PEAR_Registry::parsePackageName()} or a dependency + * returned from getDepDownloadURL() + */ + function isEqual($param) + { + if (is_object($param)) { + $channel = $param->getChannel(); + $package = $param->getPackage(); + if ($param->getURI()) { + $param = array( + 'channel' => $param->getChannel(), + 'package' => $param->getPackage(), + 'version' => $param->getVersion(), + 'uri' => $param->getURI(), + ); + } else { + $param = array( + 'channel' => $param->getChannel(), + 'package' => $param->getPackage(), + 'version' => $param->getVersion(), + ); + } + } else { + if (isset($param['uri'])) { + if ($this->getChannel() != '__uri') { + return false; + } + return $param['uri'] == $this->getURI(); + } + $package = isset($param['package']) ? $param['package'] : + $param['info']->getPackage(); + $channel = isset($param['channel']) ? $param['channel'] : + $param['info']->getChannel(); + if (isset($param['rel'])) { + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + $newdep = PEAR_Dependency2::normalizeDep($param); + $newdep = $newdep[0]; + } elseif (isset($param['min'])) { + $newdep = $param; + } + } + if (isset($newdep)) { + if (!isset($newdep['min'])) { + $newdep['min'] = '0'; + } + if (!isset($newdep['max'])) { + $newdep['max'] = '100000000000000000000'; + } + // use magic to support pecl packages suddenly jumping to the pecl channel + // we need to support both dependency possibilities + if ($channel == 'pear.php.net' && $this->getChannel() == 'pecl.php.net') { + if ($package == $this->getPackage()) { + $channel = 'pecl.php.net'; + } + } + if ($channel == 'pecl.php.net' && $this->getChannel() == 'pear.php.net') { + if ($package == $this->getPackage()) { + $channel = 'pear.php.net'; + } + } + return (strtolower($package) == strtolower($this->getPackage()) && + $channel == $this->getChannel() && + version_compare($newdep['min'], $this->getVersion(), '<=') && + version_compare($newdep['max'], $this->getVersion(), '>=')); + } + // use magic to support pecl packages suddenly jumping to the pecl channel + if ($channel == 'pecl.php.net' && $this->getChannel() == 'pear.php.net') { + if (strtolower($package) == strtolower($this->getPackage())) { + $channel = 'pear.php.net'; + } + } + if (isset($param['version'])) { + return (strtolower($package) == strtolower($this->getPackage()) && + $channel == $this->getChannel() && + $param['version'] == $this->getVersion()); + } else { + return strtolower($package) == strtolower($this->getPackage()) && + $channel == $this->getChannel(); + } + } + + function isInstalled($dep, $oper = '==') + { + if (!$dep) { + return false; + } + if ($oper != 'ge' && $oper != 'gt' && $oper != 'has' && $oper != '==') { + return false; + } + if (is_object($dep)) { + $package = $dep->getPackage(); + $channel = $dep->getChannel(); + if ($dep->getURI()) { + $dep = array( + 'uri' => $dep->getURI(), + 'version' => $dep->getVersion(), + ); + } else { + $dep = array( + 'version' => $dep->getVersion(), + ); + } + } else { + if (isset($dep['uri'])) { + $channel = '__uri'; + $package = $dep['dep']['name']; + } else { + $channel = $dep['info']->getChannel(); + $package = $dep['info']->getPackage(); + } + } + $options = $this->_downloader->getOptions(); + $test = $this->_installRegistry->packageExists($package, $channel); + if (!$test && $channel == 'pecl.php.net') { + // do magic to allow upgrading from old pecl packages to new ones + $test = $this->_installRegistry->packageExists($package, 'pear.php.net'); + $channel = 'pear.php.net'; + } + if ($test) { + if (isset($dep['uri'])) { + if ($this->_installRegistry->packageInfo($package, 'uri', '__uri') == $dep['uri']) { + return true; + } + } + if (isset($options['upgrade'])) { + if ($oper == 'has') { + if (version_compare($this->_installRegistry->packageInfo( + $package, 'version', $channel), + $dep['version'], '>=')) { + return true; + } else { + return false; + } + } else { + if (version_compare($this->_installRegistry->packageInfo( + $package, 'version', $channel), + $dep['version'], '>=')) { + return true; + } + return false; + } + } + return true; + } + return false; + } + + /** + * Detect duplicate package names with differing versions + * + * If a user requests to install Date 1.4.6 and Date 1.4.7, + * for instance, this is a logic error. This method + * detects this situation. + * + * @param array $params array of PEAR_Downloader_Package objects + * @param array $errorparams empty array + * @return array array of stupid duplicated packages in PEAR_Downloader_Package obejcts + */ + function detectStupidDuplicates($params, &$errorparams) + { + $existing = array(); + foreach ($params as $i => $param) { + if (!isset($existing[$param->getChannel() . '/' . $param->getPackage()])) { + $existing[$param->getChannel() . '/' . $param->getPackage()] = array(); + } + if (!isset($existing[$param->getChannel() . '/' . $param->getPackage()] + [$param->getGroup()])) { + $existing[$param->getChannel() . '/' . $param->getPackage()] + [$param->getGroup()] = array(); + } + $existing[$param->getChannel() . '/' . $param->getPackage()] + [$param->getGroup()][] = $i; + } + $indices = array(); + foreach ($existing as $package => $groups) { + foreach ($groups as $group => $dupes) { + if (count($dupes) > 1) { + $indices = $indices + $dupes; + } + } + } + $indices = array_unique($indices); + foreach ($indices as $index) { + $errorparams[] = $params[$index]; + } + return count($errorparams); + } + + /** + * @param array + * @param bool ignore install groups - for final removal of dupe packages + * @static + */ + function removeDuplicates(&$params, $ignoreGroups = false) + { + $pnames = array(); + foreach ($params as $i => $param) { + if (!$param) { + continue; + } + if ($param->getPackage()) { + if ($ignoreGroups) { + $group = ''; + } else { + $group = $param->getGroup(); + } + $pnames[$i] = $param->getChannel() . '/' . + $param->getPackage() . '-' . $param->getVersion() . '#' . $group; + } + } + $pnames = array_unique($pnames); + $unset = array_diff(array_keys($params), array_keys($pnames)); + $testp = array_flip($pnames); + foreach ($params as $i => $param) { + if (!$param) { + $unset[] = $i; + continue; + } + if (!is_a($param, 'PEAR_Downloader_Package')) { + $unset[] = $i; + continue; + } + if ($ignoreGroups) { + $group = ''; + } else { + $group = $param->getGroup(); + } + if (!isset($testp[$param->getChannel() . '/' . $param->getPackage() . '-' . + $param->getVersion() . '#' . $group])) { + $unset[] = $i; + } + } + foreach ($unset as $i) { + unset($params[$i]); + } + $ret = array(); + foreach ($params as $i => $param) { + $ret[] = &$params[$i]; + } + $params = array(); + foreach ($ret as $i => $param) { + $params[] = &$ret[$i]; + } + } + + function explicitState() + { + return $this->_explicitState; + } + + function setExplicitState($s) + { + $this->_explicitState = $s; + } + + /** + * @static + */ + function mergeDependencies(&$params) + { + $newparams = array(); + $bundles = array(); + foreach ($params as $i => $param) { + if (!$param->isBundle()) { + continue; + } + $bundles[] = $i; + $pf = &$param->getPackageFile(); + $newdeps = array(); + $contents = $pf->getBundledPackages(); + if (!is_array($contents)) { + $contents = array($contents); + } + foreach ($contents as $file) { + $filecontents = $pf->getFileContents($file); + $dl = &$param->getDownloader(); + $options = $dl->getOptions(); + if (PEAR::isError($dir = $dl->getDownloadDir())) { + return $dir; + } + $fp = @fopen($dir . DIRECTORY_SEPARATOR . $file, 'wb'); + if (!$fp) { + continue; + } + fwrite($fp, $filecontents, strlen($filecontents)); + fclose($fp); + if ($s = $params[$i]->explicitState()) { + $obj->setExplicitState($s); + } + $obj = &new PEAR_Downloader_Package($params[$i]->getDownloader()); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + if (PEAR::isError($dir = $dl->getDownloadDir())) { + PEAR::popErrorHandling(); + return $dir; + } + $e = $obj->_fromFile($a = $dir . DIRECTORY_SEPARATOR . $file); + PEAR::popErrorHandling(); + if (PEAR::isError($e)) { + if (!isset($options['soft'])) { + $dl->log(0, $e->getMessage()); + } + continue; + } + $j = &$obj; + if (!PEAR_Downloader_Package::willDownload($j, + array_merge($params, $newparams)) && !$param->isInstalled($j)) { + $newparams[] = &$j; + } + } + } + foreach ($bundles as $i) { + unset($params[$i]); // remove bundles - only their contents matter for installation + } + PEAR_Downloader_Package::removeDuplicates($params); // strip any unset indices + if (count($newparams)) { // add in bundled packages for install + foreach ($newparams as $i => $unused) { + $params[] = &$newparams[$i]; + } + $newparams = array(); + } + foreach ($params as $i => $param) { + $newdeps = array(); + foreach ($param->_downloadDeps as $dep) { + if (!PEAR_Downloader_Package::willDownload($dep, + array_merge($params, $newparams)) && !$param->isInstalled($dep)) { + $newdeps[] = $dep; + } else { + // detect versioning conflicts here + } + } + // convert the dependencies into PEAR_Downloader_Package objects for the next time + // around + $params[$i]->_downloadDeps = array(); + foreach ($newdeps as $dep) { + $obj = &new PEAR_Downloader_Package($params[$i]->getDownloader()); + if ($s = $params[$i]->explicitState()) { + $obj->setExplicitState($s); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $e = $obj->fromDepURL($dep); + PEAR::popErrorHandling(); + if (PEAR::isError($e)) { + if (!isset($options['soft'])) { + $obj->_downloader->log(0, $e->getMessage()); + } + continue; + } + $e = $obj->detectDependencies($params); + if (PEAR::isError($e)) { + if (!isset($options['soft'])) { + $obj->_downloader->log(0, $e->getMessage()); + } + } + $j = &$obj; + $newparams[] = &$j; + } + } + if (count($newparams)) { + foreach ($newparams as $i => $unused) { + $params[] = &$newparams[$i]; + } + return true; + } else { + return false; + } + } + + + /** + * @static + */ + function willDownload($param, $params) + { + if (!is_array($params)) { + return false; + } + foreach ($params as $obj) { + if ($obj->isEqual($param)) { + return true; + } + } + return false; + } + + /** + * For simpler unit-testing + * @param PEAR_Config + * @param int + * @param string + */ + function &getPackagefileObject(&$c, $d, $t = false) + { + $a = &new PEAR_PackageFile($c, $d, $t); + return $a; + } + + + /** + * This will retrieve from a local file if possible, and parse out + * a group name as well. The original parameter will be modified to reflect this. + * @param string|array can be a parsed package name as well + * @access private + */ + function _fromFile(&$param) + { + $saveparam = $param; + if (is_string($param)) { + if (!@file_exists($param)) { + $test = explode('#', $param); + $group = array_pop($test); + if (@file_exists(implode('#', $test))) { + $this->setGroup($group); + $param = implode('#', $test); + $this->_explicitGroup = true; + } + } + if (@is_file($param)) { + $this->_type = 'local'; + $options = $this->_downloader->getOptions(); + if (isset($options['downloadonly'])) { + $pkg = &$this->getPackagefileObject($this->_config, + $this->_downloader->_debug); + } else { + if (PEAR::isError($dir = $this->_downloader->getDownloadDir())) { + return $dir; + } + $pkg = &$this->getPackagefileObject($this->_config, + $this->_downloader->_debug, $dir); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pf = &$pkg->fromAnyFile($param, PEAR_VALIDATE_INSTALLING); + PEAR::popErrorHandling(); + if (PEAR::isError($pf)) { + $this->_valid = false; + $param = $saveparam; + return $pf; + } + $this->_packagefile = &$pf; + if (!$this->getGroup()) { + $this->setGroup('default'); // install the default dependency group + } + return $this->_valid = true; + } + } + $param = $saveparam; + return $this->_valid = false; + } + + function _fromUrl($param, $saveparam = '') + { + if (!is_array($param) && + (preg_match('#^(http|ftp)://#', $param))) { + $options = $this->_downloader->getOptions(); + $this->_type = 'url'; + $callback = $this->_downloader->ui ? + array(&$this->_downloader, '_downloadCallback') : null; + $this->_downloader->pushErrorHandling(PEAR_ERROR_RETURN); + if (PEAR::isError($dir = $this->_downloader->getDownloadDir())) { + $this->_downloader->popErrorHandling(); + return $dir; + } + $this->_downloader->log(3, 'Downloading "' . $param . '"'); + $file = $this->_downloader->downloadHttp($param, $this->_downloader->ui, + $dir, $callback); + $this->_downloader->popErrorHandling(); + if (PEAR::isError($file)) { + if (!empty($saveparam)) { + $saveparam = ", cannot download \"$saveparam\""; + } + $err = PEAR::raiseError('Could not download from "' . $param . + '"' . $saveparam . ' (' . $file->getMessage() . ')'); + return $err; + } + if ($this->_rawpackagefile) { + require_once 'Archive/Tar.php'; + $tar = &new Archive_Tar($file); + $packagexml = $tar->extractInString('package2.xml'); + if (!$packagexml) { + $packagexml = $tar->extractInString('package.xml'); + } + if (str_replace(array("\n", "\r"), array('',''), $packagexml) != + str_replace(array("\n", "\r"), array('',''), $this->_rawpackagefile)) { + if ($this->getChannel() == 'pear.php.net') { + // be more lax for the existing PEAR packages that have not-ok + // characters in their package.xml + $this->_downloader->log(0, 'CRITICAL WARNING: The "' . + $this->getPackage() . '" package has invalid characters in its ' . + 'package.xml. The next version of PEAR may not be able to install ' . + 'this package for security reasons. Please open a bug report at ' . + 'http://pear.php.net/package/' . $this->getPackage() . '/bugs'); + } else { + return PEAR::raiseError('CRITICAL ERROR: package.xml downloaded does ' . + 'not match value returned from xml-rpc'); + } + } + } + // whew, download worked! + if (isset($options['downloadonly'])) { + $pkg = &$this->getPackagefileObject($this->_config, $this->_downloader->debug); + } else { + if (PEAR::isError($dir = $this->_downloader->getDownloadDir())) { + return $dir; + } + $pkg = &$this->getPackagefileObject($this->_config, $this->_downloader->debug, + $dir); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pf = &$pkg->fromAnyFile($file, PEAR_VALIDATE_INSTALLING); + PEAR::popErrorHandling(); + if (PEAR::isError($pf)) { + if (is_array($pf->getUserInfo())) { + foreach ($pf->getUserInfo() as $err) { + if (is_array($err)) { + $err = $err['message']; + } + if (!isset($options['soft'])) { + $this->_downloader->log(0, "Validation Error: $err"); + } + } + } + if (!isset($options['soft'])) { + $this->_downloader->log(0, $pf->getMessage()); + } + $err = PEAR::raiseError('Download of "' . ($saveparam ? $saveparam : + $param) . '" succeeded, but it is not a valid package archive'); + $this->_valid = false; + return $err; + } + $this->_packagefile = &$pf; + $this->setGroup('default'); // install the default dependency group + return $this->_valid = true; + } + return $this->_valid = false; + } + + /** + * + * @param string|array pass in an array of format + * array( + * 'package' => 'pname', + * ['channel' => 'channame',] + * ['version' => 'version',] + * ['state' => 'state',]) + * or a string of format [channame/]pname[-version|-state] + */ + function _fromString($param) + { + $options = $this->_downloader->getOptions(); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pname = $this->_registry->parsePackageName($param, + $this->_config->get('default_channel')); + PEAR::popErrorHandling(); + if (PEAR::isError($pname)) { + if ($pname->getCode() == 'invalid') { + $this->_valid = false; + return false; + } + if ($pname->getCode() == 'channel') { + $parsed = $pname->getUserInfo(); + if ($this->_downloader->discover($parsed['channel'])) { + if ($this->_config->get('auto_discover')) { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pname = $this->_registry->parsePackageName($param, + $this->_config->get('default_channel')); + PEAR::popErrorHandling(); + } else { + if (!isset($options['soft'])) { + $this->_downloader->log(0, 'Channel "' . $parsed['channel'] . + '" is not initialized, use ' . + '"pear channel-discover ' . $parsed['channel'] . '" to initialize' . + 'or pear config-set auto_discover 1'); + } + } + } + if (PEAR::isError($pname)) { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $pname->getMessage()); + } + if (is_array($param)) { + $param = $this->_registry->parsedPackageNameToString($param); + } + $err = PEAR::raiseError('invalid package name/package file "' . + $param . '"'); + $this->_valid = false; + return $err; + } + } else { + if (!isset($options['soft'])) { + $this->_downloader->log(0, $pname->getMessage()); + } + $err = PEAR::raiseError('invalid package name/package file "' . + $param . '"'); + $this->_valid = false; + return $err; + } + } + if (!isset($this->_type)) { + $this->_type = 'xmlrpc'; + } + $this->_parsedname = $pname; + if (isset($pname['state'])) { + $this->_explicitState = $pname['state']; + } else { + $this->_explicitState = false; + } + if (isset($pname['group'])) { + $this->_explicitGroup = true; + } else { + $this->_explicitGroup = false; + } + $info = $this->_downloader->_getPackageDownloadUrl($pname); + if (PEAR::isError($info)) { + if ($info->getCode() != -976 && $pname['channel'] == 'pear.php.net') { + // try pecl + $pname['channel'] = 'pecl.php.net'; + if ($test = $this->_downloader->_getPackageDownloadUrl($pname)) { + if (!PEAR::isError($test)) { + $info = PEAR::raiseError($info->getMessage() . ' - package ' . + $this->_registry->parsedPackageNameToString($pname, true) . + ' can be installed with "pecl install ' . $pname['package'] . + '"'); + } else { + $pname['channel'] = 'pear.php.net'; + } + } else { + $pname['channel'] = 'pear.php.net'; + } + } + return $info; + } + $this->_rawpackagefile = $info['raw']; + $ret = $this->_analyzeDownloadURL($info, $param, $pname); + if (PEAR::isError($ret)) { + return $ret; + } + if ($ret) { + $this->_downloadURL = $ret; + return $this->_valid = (bool) $ret; + } + } + + /** + * @param array output of package.getDownloadURL + * @param string|array|object information for detecting packages to be downloaded, and + * for errors + * @param array name information of the package + * @param array|null packages to be downloaded + * @param bool is this an optional dependency? + * @param bool is this any kind of dependency? + * @access private + */ + function _analyzeDownloadURL($info, $param, $pname, $params = null, $optional = false, + $isdependency = false) + { + if (!is_string($param) && PEAR_Downloader_Package::willDownload($param, $params)) { + return false; + } + if (!$info) { + if (!is_string($param)) { + $saveparam = ", cannot download \"$param\""; + } else { + $saveparam = ''; + } + // no releases exist + return PEAR::raiseError('No releases for package "' . + $this->_registry->parsedPackageNameToString($pname, true) . '" exist' . $saveparam); + } + if (strtolower($info['info']->getChannel()) != strtolower($pname['channel'])) { + $err = false; + if ($pname['channel'] == 'pecl.php.net') { + if ($info['info']->getChannel() != 'pear.php.net') { + $err = true; + } + } elseif ($info['info']->getChannel() == 'pecl.php.net') { + if ($pname['channel'] != 'pear.php.net') { + $err = true; + } + } else { + $err = true; + } + if ($err) { + return PEAR::raiseError('SECURITY ERROR: package in channel "' . $pname['channel'] . + '" retrieved another channel\'s name for download! ("' . + $info['info']->getChannel() . '")'); + } + } + if (!isset($info['url'])) { + if ($this->isInstalled($info)) { + if ($isdependency && version_compare($info['version'], + $this->_registry->packageInfo($info['info']->getPackage(), + 'version', $info['info']->getChannel()), '<=')) { + // ignore bogus errors of "failed to download dependency" + // if it is already installed and the one that would be + // downloaded is older or the same version (Bug #7219) + return false; + } + } + $instead = ', will instead download version ' . $info['version'] . + ', stability "' . $info['info']->getState() . '"'; + // releases exist, but we failed to get any + if (isset($this->_downloader->_options['force'])) { + if (isset($pname['version'])) { + $vs = ', version "' . $pname['version'] . '"'; + } elseif (isset($pname['state'])) { + $vs = ', stability "' . $pname['state'] . '"'; + } elseif ($param == 'dependency') { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (!in_array($info['info']->getState(), + PEAR_Common::betterStates($this->_config->get('preferred_state'), true))) { + if ($optional) { + // don't spit out confusing error message + return $this->_downloader->_getPackageDownloadUrl( + array('package' => $pname['package'], + 'channel' => $pname['channel'], + 'version' => $info['version'])); + } + $vs = ' within preferred state "' . $this->_config->get('preferred_state') . + '"'; + } else { + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + if ($optional) { + // don't spit out confusing error message + return $this->_downloader->_getPackageDownloadUrl( + array('package' => $pname['package'], + 'channel' => $pname['channel'], + 'version' => $info['version'])); + } + $vs = PEAR_Dependency2::_getExtraString($pname); + $instead = ''; + } + } else { + $vs = ' within preferred state "' . $this->_config->get( + 'preferred_state') . '"'; + } + if (!isset($options['soft'])) { + $this->_downloader->log(1, 'WARNING: failed to download ' . $pname['channel'] . + '/' . $pname['package'] . $vs . $instead); + } + // download the latest release + return $this->_downloader->_getPackageDownloadUrl( + array('package' => $pname['package'], + 'channel' => $pname['channel'], + 'version' => $info['version'])); + } else { + if (isset($info['php']) && $info['php']) { + $err = PEAR::raiseError('Failed to download ' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], + 'package' => $pname['package']), + true) . + ', latest release is version ' . $info['php']['v'] . + ', but it requires PHP version "' . + $info['php']['m'] . '", use "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], 'package' => $pname['package'], + 'version' => $info['php']['v'])) . '" to install', + PEAR_DOWNLOADER_PACKAGE_PHPVERSION); + return $err; + } + // construct helpful error message + if (isset($pname['version'])) { + $vs = ', version "' . $pname['version'] . '"'; + } elseif (isset($pname['state'])) { + $vs = ', stability "' . $pname['state'] . '"'; + } elseif ($param == 'dependency') { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (!in_array($info['info']->getState(), + PEAR_Common::betterStates($this->_config->get('preferred_state'), true))) { + if ($optional) { + // don't spit out confusing error message, and don't die on + // optional dep failure! + return $this->_downloader->_getPackageDownloadUrl( + array('package' => $pname['package'], + 'channel' => $pname['channel'], + 'version' => $info['version'])); + } + $vs = ' within preferred state "' . $this->_config->get('preferred_state') . + '"'; + } else { + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + if ($optional) { + // don't spit out confusing error message, and don't die on + // optional dep failure! + return $this->_downloader->_getPackageDownloadUrl( + array('package' => $pname['package'], + 'channel' => $pname['channel'], + 'version' => $info['version'])); + } + $vs = PEAR_Dependency2::_getExtraString($pname); + } + } else { + $vs = ' within preferred state "' . $this->_downloader->config->get( + 'preferred_state') . '"'; + } + $options = $this->_downloader->getOptions(); + // this is only set by the "download-all" command + if (isset($options['ignorepreferred_state'])) { + $err = PEAR::raiseError( + 'Failed to download ' . $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], 'package' => $pname['package']), + true) + . $vs . + ', latest release is version ' . $info['version'] . + ', stability "' . $info['info']->getState() . '", use "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], 'package' => $pname['package'], + 'version' => $info['version'])) . '" to install', + PEAR_DOWNLOADER_PACKAGE_STATE); + return $err; + } + $err = PEAR::raiseError( + 'Failed to download ' . $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], 'package' => $pname['package']), + true) + . $vs . + ', latest release is version ' . $info['version'] . + ', stability "' . $info['info']->getState() . '", use "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pname['channel'], 'package' => $pname['package'], + 'version' => $info['version'])) . '" to install'); + return $err; + } + } + if (isset($info['deprecated']) && $info['deprecated']) { + $this->_downloader->log(0, + 'WARNING: "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $info['info']->getChannel(), + 'package' => $info['info']->getPackage()), true) . + '" is deprecated in favor of "' . + $this->_registry->parsedPackageNameToString($info['deprecated'], true) . + '"'); + } + return $info; + } +} +?> diff --git a/PEAR/ErrorStack.php b/PEAR/ErrorStack.php new file mode 100644 index 0000000..514d5e3 --- /dev/null +++ b/PEAR/ErrorStack.php @@ -0,0 +1,985 @@ + + * @copyright 2004-2006 Greg Beaver + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ErrorStack.php,v 1.27 2007/06/10 04:34:19 cellog Exp $ + * @link http://pear.php.net/package/PEAR_ErrorStack + */ + +/** + * Singleton storage + * + * Format: + *
+ * array(
+ *  'package1' => PEAR_ErrorStack object,
+ *  'package2' => PEAR_ErrorStack object,
+ *  ...
+ * )
+ * 
+ * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] + */ +$GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] = array(); + +/** + * Global error callback (default) + * + * This is only used if set to non-false. * is the default callback for + * all packages, whereas specific packages may set a default callback + * for all instances, regardless of whether they are a singleton or not. + * + * To exclude non-singletons, only set the local callback for the singleton + * @see PEAR_ErrorStack::setDefaultCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] = array( + '*' => false, +); + +/** + * Global Log object (default) + * + * This is only used if set to non-false. Use to set a default log object for + * all stacks, regardless of instantiation order or location + * @see PEAR_ErrorStack::setDefaultLogger() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = false; + +/** + * Global Overriding Callback + * + * This callback will override any error callbacks that specific loggers have set. + * Use with EXTREME caution + * @see PEAR_ErrorStack::staticPushCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + +/**#@+ + * One of four possible return values from the error Callback + * @see PEAR_ErrorStack::_errorCallback() + */ +/** + * If this is returned, then the error will be both pushed onto the stack + * and logged. + */ +define('PEAR_ERRORSTACK_PUSHANDLOG', 1); +/** + * If this is returned, then the error will only be pushed onto the stack, + * and not logged. + */ +define('PEAR_ERRORSTACK_PUSH', 2); +/** + * If this is returned, then the error will only be logged, but not pushed + * onto the error stack. + */ +define('PEAR_ERRORSTACK_LOG', 3); +/** + * If this is returned, then the error is completely ignored. + */ +define('PEAR_ERRORSTACK_IGNORE', 4); +/** + * If this is returned, then the error is logged and die() is called. + */ +define('PEAR_ERRORSTACK_DIE', 5); +/**#@-*/ + +/** + * Error code for an attempt to instantiate a non-class as a PEAR_ErrorStack in + * the singleton method. + */ +define('PEAR_ERRORSTACK_ERR_NONCLASS', 1); + +/** + * Error code for an attempt to pass an object into {@link PEAR_ErrorStack::getMessage()} + * that has no __toString() method + */ +define('PEAR_ERRORSTACK_ERR_OBJTOSTRING', 2); +/** + * Error Stack Implementation + * + * Usage: + * + * // global error stack + * $global_stack = &PEAR_ErrorStack::singleton('MyPackage'); + * // local error stack + * $local_stack = new PEAR_ErrorStack('MyPackage'); + * + * @author Greg Beaver + * @version 1.6.1 + * @package PEAR_ErrorStack + * @category Debugging + * @copyright 2004-2006 Greg Beaver + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ErrorStack.php,v 1.27 2007/06/10 04:34:19 cellog Exp $ + * @link http://pear.php.net/package/PEAR_ErrorStack + */ +class PEAR_ErrorStack { + /** + * Errors are stored in the order that they are pushed on the stack. + * @since 0.4alpha Errors are no longer organized by error level. + * This renders pop() nearly unusable, and levels could be more easily + * handled in a callback anyway + * @var array + * @access private + */ + var $_errors = array(); + + /** + * Storage of errors by level. + * + * Allows easy retrieval and deletion of only errors from a particular level + * @since PEAR 1.4.0dev + * @var array + * @access private + */ + var $_errorsByLevel = array(); + + /** + * Package name this error stack represents + * @var string + * @access protected + */ + var $_package; + + /** + * Determines whether a PEAR_Error is thrown upon every error addition + * @var boolean + * @access private + */ + var $_compat = false; + + /** + * If set to a valid callback, this will be used to generate the error + * message from the error code, otherwise the message passed in will be + * used + * @var false|string|array + * @access private + */ + var $_msgCallback = false; + + /** + * If set to a valid callback, this will be used to generate the error + * context for an error. For PHP-related errors, this will be a file + * and line number as retrieved from debug_backtrace(), but can be + * customized for other purposes. The error might actually be in a separate + * configuration file, or in a database query. + * @var false|string|array + * @access protected + */ + var $_contextCallback = false; + + /** + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one an PEAR_ERRORSTACK_* constant + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @var false|string|array + * @access protected + */ + var $_errorCallback = array(); + + /** + * PEAR::Log object for logging errors + * @var false|Log + * @access protected + */ + var $_logger = false; + + /** + * Error messages - designed to be overridden + * @var array + * @abstract + */ + var $_errorMsgs = array(); + + /** + * Set up a new error stack + * + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + */ + function PEAR_ErrorStack($package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false) + { + $this->_package = $package; + $this->setMessageCallback($msgCallback); + $this->setContextCallback($contextCallback); + $this->_compat = $throwPEAR_Error; + } + + /** + * Return a single error stack for this package. + * + * Note that all parameters are ignored if the stack for package $package + * has already been instantiated + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + * @param string $stackClass class to instantiate + * @static + * @return PEAR_ErrorStack + */ + function &singleton($package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false, $stackClass = 'PEAR_ErrorStack') + { + if (isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + if (!class_exists($stackClass)) { + if (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + } + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_NONCLASS, + 'exception', array('stackclass' => $stackClass), + 'stack class "%stackclass%" is not a valid class name (should be like PEAR_ErrorStack)', + false, $trace); + } + $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package] = + new $stackClass($package, $msgCallback, $contextCallback, $throwPEAR_Error); + + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + + /** + * Internal error handler for PEAR_ErrorStack class + * + * Dies if the error is an exception (and would have died anyway) + * @access private + */ + function _handleError($err) + { + if ($err['level'] == 'exception') { + $message = $err['message']; + if (isset($_SERVER['REQUEST_URI'])) { + echo '
'; + } else { + echo "\n"; + } + var_dump($err['context']); + die($message); + } + } + + /** + * Set up a PEAR::Log object for all error stacks that don't have one + * @param Log $log + * @static + */ + function setDefaultLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } elseif (is_callable($log)) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } + } + + /** + * Set up a PEAR::Log object for this error stack + * @param Log $log + */ + function setLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $this->_logger = &$log; + } elseif (is_callable($log)) { + $this->_logger = &$log; + } + } + + /** + * Set an error code => error message mapping callback + * + * This method sets the callback that can be used to generate error + * messages for any instance + * @param array|string Callback function/method + */ + function setMessageCallback($msgCallback) + { + if (!$msgCallback) { + $this->_msgCallback = array(&$this, 'getErrorMessage'); + } else { + if (is_callable($msgCallback)) { + $this->_msgCallback = $msgCallback; + } + } + } + + /** + * Get an error code => error message mapping callback + * + * This method returns the current callback that can be used to generate error + * messages + * @return array|string|false Callback function/method or false if none + */ + function getMessageCallback() + { + return $this->_msgCallback; + } + + /** + * Sets a default callback to be used by all error stacks + * + * This method sets the callback that can be used to generate error + * messages for a singleton + * @param array|string Callback function/method + * @param string Package name, or false for all packages + * @static + */ + function setDefaultCallback($callback = false, $package = false) + { + if (!is_callable($callback)) { + $callback = false; + } + $package = $package ? $package : '*'; + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$package] = $callback; + } + + /** + * Set a callback that generates context information (location of error) for an error stack + * + * This method sets the callback that can be used to generate context + * information for an error. Passing in NULL will disable context generation + * and remove the expensive call to debug_backtrace() + * @param array|string|null Callback function/method + */ + function setContextCallback($contextCallback) + { + if ($contextCallback === null) { + return $this->_contextCallback = false; + } + if (!$contextCallback) { + $this->_contextCallback = array(&$this, 'getFileLine'); + } else { + if (is_callable($contextCallback)) { + $this->_contextCallback = $contextCallback; + } + } + } + + /** + * Set an error Callback + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one of the ERRORSTACK_* constants. + * + * This functionality can be used to emulate PEAR's pushErrorHandling, and + * the PEAR_ERROR_CALLBACK mode, without affecting the integrity of + * the error stack or logging + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see popCallback() + * @param string|array $cb + */ + function pushCallback($cb) + { + array_push($this->_errorCallback, $cb); + } + + /** + * Remove a callback from the error callback stack + * @see pushCallback() + * @return array|string|false + */ + function popCallback() + { + if (!count($this->_errorCallback)) { + return false; + } + return array_pop($this->_errorCallback); + } + + /** + * Set a temporary overriding error callback for every package error stack + * + * Use this to temporarily disable all existing callbacks (can be used + * to emulate the @ operator, for instance) + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see staticPopCallback(), pushCallback() + * @param string|array $cb + * @static + */ + function staticPushCallback($cb) + { + array_push($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'], $cb); + } + + /** + * Remove a temporary overriding error callback + * @see staticPushCallback() + * @return array|string|false + * @static + */ + function staticPopCallback() + { + $ret = array_pop($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK']); + if (!is_array($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'])) { + $GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + } + return $ret; + } + + /** + * Add an error to the stack + * + * If the message generator exists, it is called with 2 parameters. + * - the current Error Stack object + * - an array that is in the same format as an error. Available indices + * are 'code', 'package', 'time', 'params', 'level', and 'context' + * + * Next, if the error should contain context information, this is + * handled by the context grabbing method. + * Finally, the error is pushed onto the proper error stack + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. If a PEAR_Error is returned, the userinfo + * property is set to the following array: + * + * + * array( + * 'code' => $code, + * 'params' => $params, + * 'package' => $this->_package, + * 'level' => $level, + * 'time' => time(), + * 'context' => $context, + * 'message' => $msg, + * //['repackage' => $err] repackaged error array/Exception class + * ); + * + * + * Normally, the previous array is returned. + */ + function push($code, $level = 'error', $params = array(), $msg = false, + $repackage = false, $backtrace = false) + { + $context = false; + // grab error context + if ($this->_contextCallback) { + if (!$backtrace) { + $backtrace = debug_backtrace(); + } + $context = call_user_func($this->_contextCallback, $code, $params, $backtrace); + } + + // save error + $time = explode(' ', microtime()); + $time = $time[1] + $time[0]; + $err = array( + 'code' => $code, + 'params' => $params, + 'package' => $this->_package, + 'level' => $level, + 'time' => $time, + 'context' => $context, + 'message' => $msg, + ); + + if ($repackage) { + $err['repackage'] = $repackage; + } + + // set up the error message, if necessary + if ($this->_msgCallback) { + $msg = call_user_func_array($this->_msgCallback, + array(&$this, $err)); + $err['message'] = $msg; + } + $push = $log = true; + $die = false; + // try the overriding callback first + $callback = $this->staticPopCallback(); + if ($callback) { + $this->staticPushCallback($callback); + } + if (!is_callable($callback)) { + // try the local callback next + $callback = $this->popCallback(); + if (is_callable($callback)) { + $this->pushCallback($callback); + } else { + // try the default callback + $callback = isset($GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package]) ? + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package] : + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK']['*']; + } + } + if (is_callable($callback)) { + switch(call_user_func($callback, $err)){ + case PEAR_ERRORSTACK_IGNORE: + return $err; + break; + case PEAR_ERRORSTACK_PUSH: + $log = false; + break; + case PEAR_ERRORSTACK_LOG: + $push = false; + break; + case PEAR_ERRORSTACK_DIE: + $die = true; + break; + // anything else returned has the same effect as pushandlog + } + } + if ($push) { + array_unshift($this->_errors, $err); + if (!isset($this->_errorsByLevel[$err['level']])) { + $this->_errorsByLevel[$err['level']] = array(); + } + $this->_errorsByLevel[$err['level']][] = &$this->_errors[0]; + } + if ($log) { + if ($this->_logger || $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']) { + $this->_log($err); + } + } + if ($die) { + die(); + } + if ($this->_compat && $push) { + return $this->raiseError($msg, $code, null, null, $err); + } + return $err; + } + + /** + * Static version of {@link push()} + * + * @param string $package Package name this error belongs to + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. see docs for {@link push()} + * @static + */ + function staticPush($package, $code, $level = 'error', $params = array(), + $msg = false, $repackage = false, $backtrace = false) + { + $s = &PEAR_ErrorStack::singleton($package); + if ($s->_contextCallback) { + if (!$backtrace) { + if (function_exists('debug_backtrace')) { + $backtrace = debug_backtrace(); + } + } + } + return $s->push($code, $level, $params, $msg, $repackage, $backtrace); + } + + /** + * Log an error using PEAR::Log + * @param array $err Error array + * @param array $levels Error level => Log constant map + * @access protected + */ + function _log($err) + { + if ($this->_logger) { + $logger = &$this->_logger; + } else { + $logger = &$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']; + } + if (is_a($logger, 'Log')) { + $levels = array( + 'exception' => PEAR_LOG_CRIT, + 'alert' => PEAR_LOG_ALERT, + 'critical' => PEAR_LOG_CRIT, + 'error' => PEAR_LOG_ERR, + 'warning' => PEAR_LOG_WARNING, + 'notice' => PEAR_LOG_NOTICE, + 'info' => PEAR_LOG_INFO, + 'debug' => PEAR_LOG_DEBUG); + if (isset($levels[$err['level']])) { + $level = $levels[$err['level']]; + } else { + $level = PEAR_LOG_INFO; + } + $logger->log($err['message'], $level, $err); + } else { // support non-standard logs + call_user_func($logger, $err); + } + } + + + /** + * Pop an error off of the error stack + * + * @return false|array + * @since 0.4alpha it is no longer possible to specify a specific error + * level to return - the last error pushed will be returned, instead + */ + function pop() + { + $err = @array_shift($this->_errors); + if (!is_null($err)) { + @array_pop($this->_errorsByLevel[$err['level']]); + if (!count($this->_errorsByLevel[$err['level']])) { + unset($this->_errorsByLevel[$err['level']]); + } + } + return $err; + } + + /** + * Pop an error off of the error stack, static method + * + * @param string package name + * @return boolean + * @since PEAR1.5.0a1 + */ + function staticPop($package) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->pop(); + } + } + + /** + * Determine whether there are any errors on the stack + * @param string|array Level name. Use to determine if any errors + * of level (string), or levels (array) have been pushed + * @return boolean + */ + function hasErrors($level = false) + { + if ($level) { + return isset($this->_errorsByLevel[$level]); + } + return count($this->_errors); + } + + /** + * Retrieve all errors since last purge + * + * @param boolean set in order to empty the error stack + * @param string level name, to return only errors of a particular severity + * @return array + */ + function getErrors($purge = false, $level = false) + { + if (!$purge) { + if ($level) { + if (!isset($this->_errorsByLevel[$level])) { + return array(); + } else { + return $this->_errorsByLevel[$level]; + } + } else { + return $this->_errors; + } + } + if ($level) { + $ret = $this->_errorsByLevel[$level]; + foreach ($this->_errorsByLevel[$level] as $i => $unused) { + // entries are references to the $_errors array + $this->_errorsByLevel[$level][$i] = false; + } + // array_filter removes all entries === false + $this->_errors = array_filter($this->_errors); + unset($this->_errorsByLevel[$level]); + return $ret; + } + $ret = $this->_errors; + $this->_errors = array(); + $this->_errorsByLevel = array(); + return $ret; + } + + /** + * Determine whether there are any errors on a single error stack, or on any error stack + * + * The optional parameter can be used to test the existence of any errors without the need of + * singleton instantiation + * @param string|false Package name to check for errors + * @param string Level name to check for a particular severity + * @return boolean + * @static + */ + function staticHasErrors($package = false, $level = false) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->hasErrors($level); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + if ($obj->hasErrors($level)) { + return true; + } + } + return false; + } + + /** + * Get a list of all errors since last purge, organized by package + * @since PEAR 1.4.0dev BC break! $level is now in the place $merge used to be + * @param boolean $purge Set to purge the error stack of existing errors + * @param string $level Set to a level name in order to retrieve only errors of a particular level + * @param boolean $merge Set to return a flat array, not organized by package + * @param array $sortfunc Function used to sort a merged array - default + * sorts by time, and should be good for most cases + * @static + * @return array + */ + function staticGetErrors($purge = false, $level = false, $merge = false, + $sortfunc = array('PEAR_ErrorStack', '_sortErrors')) + { + $ret = array(); + if (!is_callable($sortfunc)) { + $sortfunc = array('PEAR_ErrorStack', '_sortErrors'); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + $test = $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->getErrors($purge, $level); + if ($test) { + if ($merge) { + $ret = array_merge($ret, $test); + } else { + $ret[$package] = $test; + } + } + } + if ($merge) { + usort($ret, $sortfunc); + } + return $ret; + } + + /** + * Error sorting function, sorts by time + * @access private + */ + function _sortErrors($a, $b) + { + if ($a['time'] == $b['time']) { + return 0; + } + if ($a['time'] < $b['time']) { + return 1; + } + return -1; + } + + /** + * Standard file/line number/function/class context callback + * + * This function uses a backtrace generated from {@link debug_backtrace()} + * and so will not work at all in PHP < 4.3.0. The frame should + * reference the frame that contains the source of the error. + * @return array|false either array('file' => file, 'line' => line, + * 'function' => function name, 'class' => class name) or + * if this doesn't work, then false + * @param unused + * @param integer backtrace frame. + * @param array Results of debug_backtrace() + * @static + */ + function getFileLine($code, $params, $backtrace = null) + { + if ($backtrace === null) { + return false; + } + $frame = 0; + $functionframe = 1; + if (!isset($backtrace[1])) { + $functionframe = 0; + } else { + while (isset($backtrace[$functionframe]['function']) && + $backtrace[$functionframe]['function'] == 'eval' && + isset($backtrace[$functionframe + 1])) { + $functionframe++; + } + } + if (isset($backtrace[$frame])) { + if (!isset($backtrace[$frame]['file'])) { + $frame++; + } + $funcbacktrace = $backtrace[$functionframe]; + $filebacktrace = $backtrace[$frame]; + $ret = array('file' => $filebacktrace['file'], + 'line' => $filebacktrace['line']); + // rearrange for eval'd code or create function errors + if (strpos($filebacktrace['file'], '(') && + preg_match(';^(.*?)\((\d+)\) : (.*?)\\z;', $filebacktrace['file'], + $matches)) { + $ret['file'] = $matches[1]; + $ret['line'] = $matches[2] + 0; + } + if (isset($funcbacktrace['function']) && isset($backtrace[1])) { + if ($funcbacktrace['function'] != 'eval') { + if ($funcbacktrace['function'] == '__lambda_func') { + $ret['function'] = 'create_function() code'; + } else { + $ret['function'] = $funcbacktrace['function']; + } + } + } + if (isset($funcbacktrace['class']) && isset($backtrace[1])) { + $ret['class'] = $funcbacktrace['class']; + } + return $ret; + } + return false; + } + + /** + * Standard error message generation callback + * + * This method may also be called by a custom error message generator + * to fill in template values from the params array, simply + * set the third parameter to the error message template string to use + * + * The special variable %__msg% is reserved: use it only to specify + * where a message passed in by the user should be placed in the template, + * like so: + * + * Error message: %msg% - internal error + * + * If the message passed like so: + * + * + * $stack->push(ERROR_CODE, 'error', array(), 'server error 500'); + * + * + * The returned error message will be "Error message: server error 500 - + * internal error" + * @param PEAR_ErrorStack + * @param array + * @param string|false Pre-generated error message template + * @static + * @return string + */ + function getErrorMessage(&$stack, $err, $template = false) + { + if ($template) { + $mainmsg = $template; + } else { + $mainmsg = $stack->getErrorMessageTemplate($err['code']); + } + $mainmsg = str_replace('%__msg%', $err['message'], $mainmsg); + if (is_array($err['params']) && count($err['params'])) { + foreach ($err['params'] as $name => $val) { + if (is_array($val)) { + // @ is needed in case $val is a multi-dimensional array + $val = @implode(', ', $val); + } + if (is_object($val)) { + if (method_exists($val, '__toString')) { + $val = $val->__toString(); + } else { + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_OBJTOSTRING, + 'warning', array('obj' => get_class($val)), + 'object %obj% passed into getErrorMessage, but has no __toString() method'); + $val = 'Object'; + } + } + $mainmsg = str_replace('%' . $name . '%', $val, $mainmsg); + } + } + return $mainmsg; + } + + /** + * Standard Error Message Template generator from code + * @return string + */ + function getErrorMessageTemplate($code) + { + if (!isset($this->_errorMsgs[$code])) { + return '%__msg%'; + } + return $this->_errorMsgs[$code]; + } + + /** + * Set the Error Message Template array + * + * The array format must be: + *
+     * array(error code => 'message template',...)
+     * 
+ * + * Error message parameters passed into {@link push()} will be used as input + * for the error message. If the template is 'message %foo% was %bar%', and the + * parameters are array('foo' => 'one', 'bar' => 'six'), the error message returned will + * be 'message one was six' + * @return string + */ + function setErrorMessageTemplate($template) + { + $this->_errorMsgs = $template; + } + + + /** + * emulate PEAR::raiseError() + * + * @return PEAR_Error + */ + function raiseError() + { + require_once 'PEAR.php'; + $args = func_get_args(); + return call_user_func_array(array('PEAR', 'raiseError'), $args); + } +} +$stack = &PEAR_ErrorStack::singleton('PEAR_ErrorStack'); +$stack->pushCallback(array('PEAR_ErrorStack', '_handleError')); +?> diff --git a/PEAR/Exception.php b/PEAR/Exception.php new file mode 100644 index 0000000..71d2e7e --- /dev/null +++ b/PEAR/Exception.php @@ -0,0 +1,397 @@ + + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Exception.php,v 1.28 2007/05/07 01:58:54 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.3.3 + */ + + +/** + * Base PEAR_Exception Class + * + * 1) Features: + * + * - Nestable exceptions (throw new PEAR_Exception($msg, $prev_exception)) + * - Definable triggers, shot when exceptions occur + * - Pretty and informative error messages + * - Added more context info available (like class, method or cause) + * - cause can be a PEAR_Exception or an array of mixed + * PEAR_Exceptions/PEAR_ErrorStack warnings + * - callbacks for specific exception classes and their children + * + * 2) Ideas: + * + * - Maybe a way to define a 'template' for the output + * + * 3) Inherited properties from PHP Exception Class: + * + * protected $message + * protected $code + * protected $line + * protected $file + * private $trace + * + * 4) Inherited methods from PHP Exception Class: + * + * __clone + * __construct + * getMessage + * getCode + * getFile + * getLine + * getTraceSafe + * getTraceSafeAsString + * __toString + * + * 5) Usage example + * + * + * require_once 'PEAR/Exception.php'; + * + * class Test { + * function foo() { + * throw new PEAR_Exception('Error Message', ERROR_CODE); + * } + * } + * + * function myLogger($pear_exception) { + * echo $pear_exception->getMessage(); + * } + * // each time a exception is thrown the 'myLogger' will be called + * // (its use is completely optional) + * PEAR_Exception::addObserver('myLogger'); + * $test = new Test; + * try { + * $test->foo(); + * } catch (PEAR_Exception $e) { + * print $e; + * } + * + * + * @category pear + * @package PEAR + * @author Tomas V.V.Cox + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.3.3 + * + */ +class PEAR_Exception extends Exception +{ + const OBSERVER_PRINT = -2; + const OBSERVER_TRIGGER = -4; + const OBSERVER_DIE = -8; + protected $cause; + private static $_observers = array(); + private static $_uniqueid = 0; + private $_trace; + + /** + * Supported signatures: + * - PEAR_Exception(string $message); + * - PEAR_Exception(string $message, int $code); + * - PEAR_Exception(string $message, Exception $cause); + * - PEAR_Exception(string $message, Exception $cause, int $code); + * - PEAR_Exception(string $message, PEAR_Error $cause); + * - PEAR_Exception(string $message, PEAR_Error $cause, int $code); + * - PEAR_Exception(string $message, array $causes); + * - PEAR_Exception(string $message, array $causes, int $code); + * @param string exception message + * @param int|Exception|PEAR_Error|array|null exception cause + * @param int|null exception code or null + */ + public function __construct($message, $p2 = null, $p3 = null) + { + if (is_int($p2)) { + $code = $p2; + $this->cause = null; + } elseif (is_object($p2) || is_array($p2)) { + // using is_object allows both Exception and PEAR_Error + if (is_object($p2) && !($p2 instanceof Exception)) { + if (!class_exists('PEAR_Error') || !($p2 instanceof PEAR_Error)) { + throw new PEAR_Exception('exception cause must be Exception, ' . + 'array, or PEAR_Error'); + } + } + $code = $p3; + if (is_array($p2) && isset($p2['message'])) { + // fix potential problem of passing in a single warning + $p2 = array($p2); + } + $this->cause = $p2; + } else { + $code = null; + $this->cause = null; + } + parent::__construct($message, $code); + $this->signal(); + } + + /** + * @param mixed $callback - A valid php callback, see php func is_callable() + * - A PEAR_Exception::OBSERVER_* constant + * - An array(const PEAR_Exception::OBSERVER_*, + * mixed $options) + * @param string $label The name of the observer. Use this if you want + * to remove it later with removeObserver() + */ + public static function addObserver($callback, $label = 'default') + { + self::$_observers[$label] = $callback; + } + + public static function removeObserver($label = 'default') + { + unset(self::$_observers[$label]); + } + + /** + * @return int unique identifier for an observer + */ + public static function getUniqueId() + { + return self::$_uniqueid++; + } + + private function signal() + { + foreach (self::$_observers as $func) { + if (is_callable($func)) { + call_user_func($func, $this); + continue; + } + settype($func, 'array'); + switch ($func[0]) { + case self::OBSERVER_PRINT : + $f = (isset($func[1])) ? $func[1] : '%s'; + printf($f, $this->getMessage()); + break; + case self::OBSERVER_TRIGGER : + $f = (isset($func[1])) ? $func[1] : E_USER_NOTICE; + trigger_error($this->getMessage(), $f); + break; + case self::OBSERVER_DIE : + $f = (isset($func[1])) ? $func[1] : '%s'; + die(printf($f, $this->getMessage())); + break; + default: + trigger_error('invalid observer type', E_USER_WARNING); + } + } + } + + /** + * Return specific error information that can be used for more detailed + * error messages or translation. + * + * This method may be overridden in child exception classes in order + * to add functionality not present in PEAR_Exception and is a placeholder + * to define API + * + * The returned array must be an associative array of parameter => value like so: + *
+     * array('name' => $name, 'context' => array(...))
+     * 
+ * @return array + */ + public function getErrorData() + { + return array(); + } + + /** + * Returns the exception that caused this exception to be thrown + * @access public + * @return Exception|array The context of the exception + */ + public function getCause() + { + return $this->cause; + } + + /** + * Function must be public to call on caused exceptions + * @param array + */ + public function getCauseMessage(&$causes) + { + $trace = $this->getTraceSafe(); + $cause = array('class' => get_class($this), + 'message' => $this->message, + 'file' => 'unknown', + 'line' => 'unknown'); + if (isset($trace[0])) { + if (isset($trace[0]['file'])) { + $cause['file'] = $trace[0]['file']; + $cause['line'] = $trace[0]['line']; + } + } + $causes[] = $cause; + if ($this->cause instanceof PEAR_Exception) { + $this->cause->getCauseMessage($causes); + } elseif ($this->cause instanceof Exception) { + $causes[] = array('class' => get_class($this->cause), + 'message' => $this->cause->getMessage(), + 'file' => $this->cause->getFile(), + 'line' => $this->cause->getLine()); + } elseif (class_exists('PEAR_Error') && $this->cause instanceof PEAR_Error) { + $causes[] = array('class' => get_class($this->cause), + 'message' => $this->cause->getMessage(), + 'file' => 'unknown', + 'line' => 'unknown'); + } elseif (is_array($this->cause)) { + foreach ($this->cause as $cause) { + if ($cause instanceof PEAR_Exception) { + $cause->getCauseMessage($causes); + } elseif ($cause instanceof Exception) { + $causes[] = array('class' => get_class($cause), + 'message' => $cause->getMessage(), + 'file' => $cause->getFile(), + 'line' => $cause->getLine()); + } elseif (class_exists('PEAR_Error') && $cause instanceof PEAR_Error) { + $causes[] = array('class' => get_class($cause), + 'message' => $cause->getMessage(), + 'file' => 'unknown', + 'line' => 'unknown'); + } elseif (is_array($cause) && isset($cause['message'])) { + // PEAR_ErrorStack warning + $causes[] = array( + 'class' => $cause['package'], + 'message' => $cause['message'], + 'file' => isset($cause['context']['file']) ? + $cause['context']['file'] : + 'unknown', + 'line' => isset($cause['context']['line']) ? + $cause['context']['line'] : + 'unknown', + ); + } + } + } + } + + public function getTraceSafe() + { + if (!isset($this->_trace)) { + $this->_trace = $this->getTrace(); + if (empty($this->_trace)) { + $backtrace = debug_backtrace(); + $this->_trace = array($backtrace[count($backtrace)-1]); + } + } + return $this->_trace; + } + + public function getErrorClass() + { + $trace = $this->getTraceSafe(); + return $trace[0]['class']; + } + + public function getErrorMethod() + { + $trace = $this->getTraceSafe(); + return $trace[0]['function']; + } + + public function __toString() + { + if (isset($_SERVER['REQUEST_URI'])) { + return $this->toHtml(); + } + return $this->toText(); + } + + public function toHtml() + { + $trace = $this->getTraceSafe(); + $causes = array(); + $this->getCauseMessage($causes); + $html = '' . "\n"; + foreach ($causes as $i => $cause) { + $html .= '\n"; + } + $html .= '' . "\n" + . '' + . '' + . '' . "\n"; + + foreach ($trace as $k => $v) { + $html .= '' + . '' + . '' . "\n"; + } + $html .= '' + . '' + . '' . "\n" + . '
' + . str_repeat('-', $i) . ' ' . $cause['class'] . ': ' + . htmlspecialchars($cause['message']) . ' in ' . $cause['file'] . ' ' + . 'on line ' . $cause['line'] . '' + . "
Exception trace
#FunctionLocation
' . $k . ''; + if (!empty($v['class'])) { + $html .= $v['class'] . $v['type']; + } + $html .= $v['function']; + $args = array(); + if (!empty($v['args'])) { + foreach ($v['args'] as $arg) { + if (is_null($arg)) $args[] = 'null'; + elseif (is_array($arg)) $args[] = 'Array'; + elseif (is_object($arg)) $args[] = 'Object('.get_class($arg).')'; + elseif (is_bool($arg)) $args[] = $arg ? 'true' : 'false'; + elseif (is_int($arg) || is_double($arg)) $args[] = $arg; + else { + $arg = (string)$arg; + $str = htmlspecialchars(substr($arg, 0, 16)); + if (strlen($arg) > 16) $str .= '…'; + $args[] = "'" . $str . "'"; + } + } + } + $html .= '(' . implode(', ',$args) . ')' + . '' . (isset($v['file']) ? $v['file'] : 'unknown') + . ':' . (isset($v['line']) ? $v['line'] : 'unknown') + . '
' . ($k+1) . '{main} 
'; + return $html; + } + + public function toText() + { + $causes = array(); + $this->getCauseMessage($causes); + $causeMsg = ''; + foreach ($causes as $i => $cause) { + $causeMsg .= str_repeat(' ', $i) . $cause['class'] . ': ' + . $cause['message'] . ' in ' . $cause['file'] + . ' on line ' . $cause['line'] . "\n"; + } + return $causeMsg . $this->getTraceAsString(); + } +} + +?> \ No newline at end of file diff --git a/PEAR/Frontend.php b/PEAR/Frontend.php new file mode 100644 index 0000000..159c448 --- /dev/null +++ b/PEAR/Frontend.php @@ -0,0 +1,223 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Frontend.php,v 1.12 2007/05/31 03:51:08 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Which user interface class is being used. + * @var string class name + */ +$GLOBALS['_PEAR_FRONTEND_CLASS'] = 'PEAR_Frontend_CLI'; + +/** + * Instance of $_PEAR_Command_uiclass. + * @var object + */ +$GLOBALS['_PEAR_FRONTEND_SINGLETON'] = null; + +/** + * Singleton-based frontend for PEAR user input/output + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Frontend extends PEAR +{ + /** + * Retrieve the frontend object + * @return PEAR_Frontend_CLI|PEAR_Frontend_Web|PEAR_Frontend_Gtk + * @static + */ + function &singleton($type = null) + { + if ($type === null) { + if (!isset($GLOBALS['_PEAR_FRONTEND_SINGLETON'])) { + $a = false; + return $a; + } + return $GLOBALS['_PEAR_FRONTEND_SINGLETON']; + } else { + $a = PEAR_Frontend::setFrontendClass($type); + return $a; + } + } + + /** + * Set the frontend class that will be used by calls to {@link singleton()} + * + * Frontends are expected to conform to the PEAR naming standard of + * _ => DIRECTORY_SEPARATOR (PEAR_Frontend_CLI is in PEAR/Frontend/CLI.php) + * @param string $uiclass full class name + * @return PEAR_Frontend + * @static + */ + function &setFrontendClass($uiclass) + { + if (is_object($GLOBALS['_PEAR_FRONTEND_SINGLETON']) && + is_a($GLOBALS['_PEAR_FRONTEND_SINGLETON'], $uiclass)) { + return $GLOBALS['_PEAR_FRONTEND_SINGLETON']; + } + if (!class_exists($uiclass)) { + $file = str_replace('_', '/', $uiclass) . '.php'; + if (PEAR_Frontend::isIncludeable($file)) { + include_once $file; + } + } + if (class_exists($uiclass)) { + $obj = &new $uiclass; + // quick test to see if this class implements a few of the most + // important frontend methods + if (is_a($obj, 'PEAR_Frontend')) { + $GLOBALS['_PEAR_FRONTEND_SINGLETON'] = &$obj; + $GLOBALS['_PEAR_FRONTEND_CLASS'] = $uiclass; + return $obj; + } else { + $err = PEAR::raiseError("not a frontend class: $uiclass"); + return $err; + } + } + $err = PEAR::raiseError("no such class: $uiclass"); + return $err; + } + + /** + * Set the frontend class that will be used by calls to {@link singleton()} + * + * Frontends are expected to be a descendant of PEAR_Frontend + * @param PEAR_Frontend + * @return PEAR_Frontend + * @static + */ + function &setFrontendObject($uiobject) + { + if (is_object($GLOBALS['_PEAR_FRONTEND_SINGLETON']) && + is_a($GLOBALS['_PEAR_FRONTEND_SINGLETON'], get_class($uiobject))) { + return $GLOBALS['_PEAR_FRONTEND_SINGLETON']; + } + if (!is_a($uiobject, 'PEAR_Frontend')) { + $err = PEAR::raiseError('not a valid frontend class: (' . + get_class($uiobject) . ')'); + return $err; + } + $GLOBALS['_PEAR_FRONTEND_SINGLETON'] = &$uiobject; + $GLOBALS['_PEAR_FRONTEND_CLASS'] = get_class($uiobject); + return $uiobject; + } + + /** + * @param string $path relative or absolute include path + * @return boolean + * @static + */ + function isIncludeable($path) + { + if (file_exists($path) && is_readable($path)) { + return true; + } + $fp = @fopen($path, 'r', true); + if ($fp) { + fclose($fp); + return true; + } + return false; + } + + /** + * @param PEAR_Config + */ + function setConfig(&$config) + { + } + + /** + * This can be overridden to allow session-based temporary file management + * + * By default, all files are deleted at the end of a session. The web installer + * needs to be able to sustain a list over many sessions in order to support + * user interaction with install scripts + */ + function addTempFile($file) + { + $GLOBALS['_PEAR_Common_tempfiles'][] = $file; + } + + /** + * Log an action + * + * @param string $msg the message to log + * @param boolean $append_crlf + * @return boolean true + * @abstract + */ + function log($msg, $append_crlf = true) + { + } + + /** + * Run a post-installation script + * + * @param array $scripts array of post-install scripts + * @abstract + */ + function runPostinstallScripts(&$scripts) + { + } + + /** + * Display human-friendly output formatted depending on the + * $command parameter. + * + * This should be able to handle basic output data with no command + * @param mixed $data data structure containing the information to display + * @param string $command command from which this method was called + * @abstract + */ + function outputData($data, $command = '_default') + { + } + + /** + * Display a modal form dialog and return the given input + * + * A frontend that requires multiple requests to retrieve and process + * data must take these needs into account, and implement the request + * handling code. + * @param string $command command from which this method was called + * @param array $prompts associative array. keys are the input field names + * and values are the description + * @param array $types array of input field types (text, password, + * etc.) keys have to be the same like in $prompts + * @param array $defaults array of default values. again keys have + * to be the same like in $prompts. Do not depend + * on a default value being set. + * @return array input sent by the user + * @abstract + */ + function userDialog($command, $prompts, $types = array(), $defaults = array()) + { + } +} +?> \ No newline at end of file diff --git a/PEAR/Frontend/CLI.php b/PEAR/Frontend/CLI.php new file mode 100644 index 0000000..e7012cc --- /dev/null +++ b/PEAR/Frontend/CLI.php @@ -0,0 +1,794 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: CLI.php,v 1.67 2007/03/14 13:42:24 timj Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ +/** + * base class + */ +require_once 'PEAR/Frontend.php'; + +/** + * Command-line Frontend for the PEAR Installer + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Frontend_CLI extends PEAR_Frontend +{ + // {{{ properties + + /** + * What type of user interface this frontend is for. + * @var string + * @access public + */ + var $type = 'CLI'; + var $lp = ''; // line prefix + + var $params = array(); + var $term = array( + 'bold' => '', + 'normal' => '', + ); + + // }}} + + // {{{ constructor + + function PEAR_Frontend_CLI() + { + parent::PEAR(); + $term = getenv('TERM'); //(cox) $_ENV is empty for me in 4.1.1 + if (function_exists('posix_isatty') && !posix_isatty(1)) { + // output is being redirected to a file or through a pipe + } elseif ($term) { + // XXX can use ncurses extension here, if available + if (preg_match('/^(xterm|vt220|linux)/', $term)) { + $this->term['bold'] = sprintf("%c%c%c%c", 27, 91, 49, 109); + $this->term['normal']=sprintf("%c%c%c", 27, 91, 109); + } elseif (preg_match('/^vt100/', $term)) { + $this->term['bold'] = sprintf("%c%c%c%c%c%c", 27, 91, 49, 109, 0, 0); + $this->term['normal']=sprintf("%c%c%c%c%c", 27, 91, 109, 0, 0); + } + } elseif (OS_WINDOWS) { + // XXX add ANSI codes here + } + } + + // }}} + + // {{{ displayLine(text) + + function displayLine($text) + { + trigger_error("PEAR_Frontend_CLI::displayLine deprecated", E_USER_ERROR); + } + + function _displayLine($text) + { + print "$this->lp$text\n"; + } + + // }}} + // {{{ display(text) + + function display($text) + { + trigger_error("PEAR_Frontend_CLI::display deprecated", E_USER_ERROR); + } + + function _display($text) + { + print $text; + } + + // }}} + // {{{ displayError(eobj) + + /** + * @param object PEAR_Error object + */ + function displayError($eobj) + { + return $this->_displayLine($eobj->getMessage()); + } + + // }}} + // {{{ displayFatalError(eobj) + + /** + * @param object PEAR_Error object + */ + function displayFatalError($eobj) + { + $this->displayError($eobj); + if (class_exists('PEAR_Config')) { + $config = &PEAR_Config::singleton(); + if ($config->get('verbose') > 5) { + if (function_exists('debug_print_backtrace')) { + debug_print_backtrace(); + } elseif (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + $raised = false; + foreach ($trace as $i => $frame) { + if (!$raised) { + if (isset($frame['class']) && strtolower($frame['class']) == + 'pear' && strtolower($frame['function']) == 'raiseerror') { + $raised = true; + } else { + continue; + } + } + if (!isset($frame['class'])) { + $frame['class'] = ''; + } + if (!isset($frame['type'])) { + $frame['type'] = ''; + } + if (!isset($frame['function'])) { + $frame['function'] = ''; + } + if (!isset($frame['line'])) { + $frame['line'] = ''; + } + $this->_displayLine("#$i: $frame[class]$frame[type]$frame[function] $frame[line]"); + } + } + } + } + exit(1); + } + + // }}} + // {{{ displayHeading(title) + + function displayHeading($title) + { + trigger_error("PEAR_Frontend_CLI::displayHeading deprecated", E_USER_ERROR); + } + + function _displayHeading($title) + { + print $this->lp.$this->bold($title)."\n"; + print $this->lp.str_repeat("=", strlen($title))."\n"; + } + + // }}} + + /** + * Instruct the runInstallScript method to skip a paramgroup that matches the + * id value passed in. + * + * This method is useful for dynamically configuring which sections of a post-install script + * will be run based on the user's setup, which is very useful for making flexible + * post-install scripts without losing the cross-Frontend ability to retrieve user input + * @param string + */ + function skipParamgroup($id) + { + $this->_skipSections[$id] = true; + } + + function runPostinstallScripts(&$scripts) + { + foreach ($scripts as $i => $script) { + $this->runInstallScript($scripts[$i]->_params, $scripts[$i]->_obj); + } + } + + /** + * @param array $xml contents of postinstallscript tag + * @param object $script post-installation script + * @param string install|upgrade + */ + function runInstallScript($xml, &$script) + { + $this->_skipSections = array(); + if (!is_array($xml) || !isset($xml['paramgroup'])) { + $script->run(array(), '_default'); + } else { + $completedPhases = array(); + if (!isset($xml['paramgroup'][0])) { + $xml['paramgroup'] = array($xml['paramgroup']); + } + foreach ($xml['paramgroup'] as $group) { + if (isset($this->_skipSections[$group['id']])) { + // the post-install script chose to skip this section dynamically + continue; + } + if (isset($group['name'])) { + $paramname = explode('::', $group['name']); + if ($lastgroup['id'] != $paramname[0]) { + continue; + } + $group['name'] = $paramname[1]; + if (isset($answers)) { + if (isset($answers[$group['name']])) { + switch ($group['conditiontype']) { + case '=' : + if ($answers[$group['name']] != $group['value']) { + continue 2; + } + break; + case '!=' : + if ($answers[$group['name']] == $group['value']) { + continue 2; + } + break; + case 'preg_match' : + if (!@preg_match('/' . $group['value'] . '/', + $answers[$group['name']])) { + continue 2; + } + break; + default : + return; + } + } + } else { + return; + } + } + $lastgroup = $group; + if (isset($group['instructions'])) { + $this->_display($group['instructions']); + } + if (!isset($group['param'][0])) { + $group['param'] = array($group['param']); + } + if (isset($group['param'])) { + if (method_exists($script, 'postProcessPrompts')) { + $prompts = $script->postProcessPrompts($group['param'], $group['id']); + if (!is_array($prompts) || count($prompts) != count($group['param'])) { + $this->outputData('postinstall', 'Error: post-install script did not ' . + 'return proper post-processed prompts'); + $prompts = $group['param']; + } else { + foreach ($prompts as $i => $var) { + if (!is_array($var) || !isset($var['prompt']) || + !isset($var['name']) || + ($var['name'] != $group['param'][$i]['name']) || + ($var['type'] != $group['param'][$i]['type'])) { + $this->outputData('postinstall', 'Error: post-install script ' . + 'modified the variables or prompts, severe security risk. ' . + 'Will instead use the defaults from the package.xml'); + $prompts = $group['param']; + } + } + } + $answers = $this->confirmDialog($prompts); + } else { + $answers = $this->confirmDialog($group['param']); + } + } + if ((isset($answers) && $answers) || !isset($group['param'])) { + if (!isset($answers)) { + $answers = array(); + } + array_unshift($completedPhases, $group['id']); + if (!$script->run($answers, $group['id'])) { + $script->run($completedPhases, '_undoOnError'); + return; + } + } else { + $script->run($completedPhases, '_undoOnError'); + return; + } + } + } + } + + /** + * Ask for user input, confirm the answers and continue until the user is satisfied + * @param array an array of arrays, format array('name' => 'paramname', 'prompt' => + * 'text to display', 'type' => 'string'[, default => 'default value']) + * @return array + */ + function confirmDialog($params) + { + $answers = array(); + $prompts = $types = array(); + foreach ($params as $param) { + $prompts[$param['name']] = $param['prompt']; + $types[$param['name']] = $param['type']; + if (isset($param['default'])) { + $answers[$param['name']] = $param['default']; + } else { + $answers[$param['name']] = ''; + } + } + $tried = false; + do { + if ($tried) { + $i = 1; + foreach ($answers as $var => $value) { + if (!strlen($value)) { + echo $this->bold("* Enter an answer for #" . $i . ": ({$prompts[$var]})\n"); + } + $i++; + } + } + $answers = $this->userDialog('', $prompts, $types, $answers); + $tried = true; + } while (is_array($answers) && count(array_filter($answers)) != count($prompts)); + return $answers; + } + // {{{ userDialog(prompt, [type], [default]) + + function userDialog($command, $prompts, $types = array(), $defaults = array(), + $screensize = 20) + { + if (!is_array($prompts)) { + return array(); + } + $testprompts = array_keys($prompts); + $result = $defaults; + if (!defined('STDIN')) { + $fp = fopen('php://stdin', 'r'); + } else { + $fp = STDIN; + } + reset($prompts); + if (count($prompts) == 1 && $types[key($prompts)] == 'yesno') { + foreach ($prompts as $key => $prompt) { + $type = $types[$key]; + $default = @$defaults[$key]; + print "$prompt "; + if ($default) { + print "[$default] "; + } + print ": "; + if (version_compare(phpversion(), '5.0.0', '<')) { + $line = fgets($fp, 2048); + } else { + if (!defined('STDIN')) { + define('STDIN', fopen('php://stdin', 'r')); + } + $line = fgets(STDIN, 2048); + } + if ($default && trim($line) == "") { + $result[$key] = $default; + } else { + $result[$key] = trim($line); + } + } + return $result; + } + while (true) { + $descLength = max(array_map('strlen', $prompts)); + $descFormat = "%-{$descLength}s"; + $last = count($prompts); + + $i = 0; + foreach ($prompts as $n => $var) { + printf("%2d. $descFormat : %s\n", ++$i, $prompts[$n], isset($result[$n]) ? + $result[$n] : null); + } + + print "\n1-$last, 'all', 'abort', or Enter to continue: "; + $tmp = trim(fgets($fp, 1024)); + if (empty($tmp)) { + break; + } + if ($tmp == 'abort') { + return false; + } + if (isset($testprompts[(int)$tmp - 1])) { + $var = $testprompts[(int)$tmp - 1]; + $desc = $prompts[$var]; + $current = @$result[$var]; + print "$desc [$current] : "; + $tmp = trim(fgets($fp, 1024)); + if (trim($tmp) !== '') { + $result[$var] = trim($tmp); + } + } elseif ($tmp == 'all') { + foreach ($prompts as $var => $desc) { + $current = $result[$var]; + print "$desc [$current] : "; + $tmp = trim(fgets($fp, 1024)); + if (trim($tmp) !== '') { + $result[$var] = trim($tmp); + } + } + } + } + if (!defined('STDIN')) { + fclose($fp); + } + return $result; + } + + // }}} + // {{{ userConfirm(prompt, [default]) + + function userConfirm($prompt, $default = 'yes') + { + trigger_error("PEAR_Frontend_CLI::userConfirm not yet converted", E_USER_ERROR); + static $positives = array('y', 'yes', 'on', '1'); + static $negatives = array('n', 'no', 'off', '0'); + print "$this->lp$prompt [$default] : "; + $fp = fopen("php://stdin", "r"); + $line = fgets($fp, 2048); + fclose($fp); + $answer = strtolower(trim($line)); + if (empty($answer)) { + $answer = $default; + } + if (in_array($answer, $positives)) { + return true; + } + if (in_array($answer, $negatives)) { + return false; + } + if (in_array($default, $positives)) { + return true; + } + return false; + } + + // }}} + // {{{ startTable([params]) + + function startTable($params = array()) + { + trigger_error("PEAR_Frontend_CLI::startTable deprecated", E_USER_ERROR); + } + + function _startTable($params = array()) + { + $params['table_data'] = array(); + $params['widest'] = array(); // indexed by column + $params['highest'] = array(); // indexed by row + $params['ncols'] = 0; + $this->params = $params; + } + + // }}} + // {{{ tableRow(columns, [rowparams], [colparams]) + + function tableRow($columns, $rowparams = array(), $colparams = array()) + { + trigger_error("PEAR_Frontend_CLI::tableRow deprecated", E_USER_ERROR); + } + + function _tableRow($columns, $rowparams = array(), $colparams = array()) + { + $highest = 1; + for ($i = 0; $i < sizeof($columns); $i++) { + $col = &$columns[$i]; + if (isset($colparams[$i]) && !empty($colparams[$i]['wrap'])) { + $col = wordwrap($col, $colparams[$i]['wrap'], "\n", 0); + } + if (strpos($col, "\n") !== false) { + $multiline = explode("\n", $col); + $w = 0; + foreach ($multiline as $n => $line) { + if (strlen($line) > $w) { + $w = strlen($line); + } + } + $lines = sizeof($multiline); + } else { + $w = strlen($col); + } + + if (isset($this->params['widest'][$i])) { + if ($w > $this->params['widest'][$i]) { + $this->params['widest'][$i] = $w; + } + } else { + $this->params['widest'][$i] = $w; + } + $tmp = count_chars($columns[$i], 1); + // handle unix, mac and windows formats + $lines = (isset($tmp[10]) ? $tmp[10] : (isset($tmp[13]) ? $tmp[13] : 0)) + 1; + if ($lines > $highest) { + $highest = $lines; + } + } + if (sizeof($columns) > $this->params['ncols']) { + $this->params['ncols'] = sizeof($columns); + } + $new_row = array( + 'data' => $columns, + 'height' => $highest, + 'rowparams' => $rowparams, + 'colparams' => $colparams, + ); + $this->params['table_data'][] = $new_row; + } + + // }}} + // {{{ endTable() + + function endTable() + { + trigger_error("PEAR_Frontend_CLI::endTable deprecated", E_USER_ERROR); + } + + function _endTable() + { + extract($this->params); + if (!empty($caption)) { + $this->_displayHeading($caption); + } + if (count($table_data) == 0) { + return; + } + if (!isset($width)) { + $width = $widest; + } else { + for ($i = 0; $i < $ncols; $i++) { + if (!isset($width[$i])) { + $width[$i] = $widest[$i]; + } + } + } + $border = false; + if (empty($border)) { + $cellstart = ''; + $cellend = ' '; + $rowend = ''; + $padrowend = false; + $borderline = ''; + } else { + $cellstart = '| '; + $cellend = ' '; + $rowend = '|'; + $padrowend = true; + $borderline = '+'; + foreach ($width as $w) { + $borderline .= str_repeat('-', $w + strlen($cellstart) + strlen($cellend) - 1); + $borderline .= '+'; + } + } + if ($borderline) { + $this->_displayLine($borderline); + } + for ($i = 0; $i < sizeof($table_data); $i++) { + extract($table_data[$i]); + if (!is_array($rowparams)) { + $rowparams = array(); + } + if (!is_array($colparams)) { + $colparams = array(); + } + $rowlines = array(); + if ($height > 1) { + for ($c = 0; $c < sizeof($data); $c++) { + $rowlines[$c] = preg_split('/(\r?\n|\r)/', $data[$c]); + if (sizeof($rowlines[$c]) < $height) { + $rowlines[$c] = array_pad($rowlines[$c], $height, ''); + } + } + } else { + for ($c = 0; $c < sizeof($data); $c++) { + $rowlines[$c] = array($data[$c]); + } + } + for ($r = 0; $r < $height; $r++) { + $rowtext = ''; + for ($c = 0; $c < sizeof($data); $c++) { + if (isset($colparams[$c])) { + $attribs = array_merge($rowparams, $colparams); + } else { + $attribs = $rowparams; + } + $w = isset($width[$c]) ? $width[$c] : 0; + //$cell = $data[$c]; + $cell = $rowlines[$c][$r]; + $l = strlen($cell); + if ($l > $w) { + $cell = substr($cell, 0, $w); + } + if (isset($attribs['bold'])) { + $cell = $this->bold($cell); + } + if ($l < $w) { + // not using str_pad here because we may + // add bold escape characters to $cell + $cell .= str_repeat(' ', $w - $l); + } + + $rowtext .= $cellstart . $cell . $cellend; + } + if (!$border) { + $rowtext = rtrim($rowtext); + } + $rowtext .= $rowend; + $this->_displayLine($rowtext); + } + } + if ($borderline) { + $this->_displayLine($borderline); + } + } + + // }}} + // {{{ outputData() + + function outputData($data, $command = '_default') + { + switch ($command) { + case 'channel-info': + foreach ($data as $type => $section) { + if ($type == 'main') { + $section['data'] = array_values($section['data']); + } + $this->outputData($section); + } + break; + case 'install': + case 'upgrade': + case 'upgrade-all': + if (isset($data['release_warnings'])) { + $this->_displayLine(''); + $this->_startTable(array( + 'border' => false, + 'caption' => 'Release Warnings' + )); + $this->_tableRow(array($data['release_warnings']), null, array(1 => array('wrap' => 55))); + $this->_endTable(); + $this->_displayLine(''); + } + $this->_displayLine($data['data']); + break; + case 'search': + $this->_startTable($data); + if (isset($data['headline']) && is_array($data['headline'])) { + $this->_tableRow($data['headline'], array('bold' => true), array(1 => array('wrap' => 55))); + } + + foreach($data['data'] as $category) { + foreach($category as $pkg) { + $this->_tableRow($pkg, null, array(1 => array('wrap' => 55))); + } + }; + $this->_endTable(); + break; + case 'list-all': + if (!isset($data['data'])) { + $this->_displayLine('No packages in channel'); + break; + } + $this->_startTable($data); + if (isset($data['headline']) && is_array($data['headline'])) { + $this->_tableRow($data['headline'], array('bold' => true), array(1 => array('wrap' => 55))); + } + + foreach($data['data'] as $category) { + foreach($category as $pkg) { + unset($pkg[4]); + unset($pkg[5]); + $this->_tableRow($pkg, null, array(1 => array('wrap' => 55))); + } + }; + $this->_endTable(); + break; + case 'config-show': + $data['border'] = false; + $opts = array(0 => array('wrap' => 30), + 1 => array('wrap' => 20), + 2 => array('wrap' => 35)); + $this->_startTable($data); + if (isset($data['headline']) && is_array($data['headline'])) { + $this->_tableRow($data['headline'], + array('bold' => true), + $opts); + } + foreach($data['data'] as $group) { + foreach($group as $value) { + if ($value[2] == '') { + $value[2] = ""; + } + $this->_tableRow($value, null, $opts); + } + } + $this->_endTable(); + break; + case 'remote-info': + $d = $data; + $data = array( + 'caption' => 'Package details:', + 'border' => false, + 'data' => array( + array("Latest", $data['stable']), + array("Installed", $data['installed']), + array("Package", $data['name']), + array("License", $data['license']), + array("Category", $data['category']), + array("Summary", $data['summary']), + array("Description", $data['description']), + ), + ); + if (isset($d['deprecated']) && $d['deprecated']) { + $conf = &PEAR_Config::singleton(); + $reg = $conf->getRegistry(); + $name = $reg->parsedPackageNameToString($d['deprecated'], true); + $data['data'][] = array('Deprecated! use', $name); + } + default: { + if (is_array($data)) { + $this->_startTable($data); + $count = count($data['data'][0]); + if ($count == 2) { + $opts = array(0 => array('wrap' => 25), + 1 => array('wrap' => 48) + ); + } elseif ($count == 3) { + $opts = array(0 => array('wrap' => 30), + 1 => array('wrap' => 20), + 2 => array('wrap' => 35) + ); + } else { + $opts = null; + } + if (isset($data['headline']) && is_array($data['headline'])) { + $this->_tableRow($data['headline'], + array('bold' => true), + $opts); + } + foreach($data['data'] as $row) { + $this->_tableRow($row, null, $opts); + } + $this->_endTable(); + } else { + $this->_displayLine($data); + } + } + } + } + + // }}} + // {{{ log(text) + + + function log($text, $append_crlf = true) + { + if ($append_crlf) { + return $this->_displayLine($text); + } + return $this->_display($text); + } + + + // }}} + // {{{ bold($text) + + function bold($text) + { + if (empty($this->term['bold'])) { + return strtoupper($text); + } + return $this->term['bold'] . $text . $this->term['normal']; + } + + // }}} +} + +?> diff --git a/PEAR/Installer.php b/PEAR/Installer.php new file mode 100644 index 0000000..941c431 --- /dev/null +++ b/PEAR/Installer.php @@ -0,0 +1,1678 @@ + + * @author Tomas V.V. Cox + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Installer.php,v 1.245 2007/06/10 04:16:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * Used for installation groups in package.xml 2.0 and platform exceptions + */ +require_once 'OS/Guess.php'; +require_once 'PEAR/Downloader.php'; + +define('PEAR_INSTALLER_NOBINARY', -240); +/** + * Administration class used to install PEAR packages and maintain the + * installed package database. + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V. Cox + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Installer extends PEAR_Downloader +{ + // {{{ properties + + /** name of the package directory, for example Foo-1.0 + * @var string + */ + var $pkgdir; + + /** directory where PHP code files go + * @var string + */ + var $phpdir; + + /** directory where PHP extension files go + * @var string + */ + var $extdir; + + /** directory where documentation goes + * @var string + */ + var $docdir; + + /** installation root directory (ala PHP's INSTALL_ROOT or + * automake's DESTDIR + * @var string + */ + var $installroot = ''; + + /** debug level + * @var int + */ + var $debug = 1; + + /** temporary directory + * @var string + */ + var $tmpdir; + + /** + * PEAR_Registry object used by the installer + * @var PEAR_Registry + */ + var $registry; + + /** + * array of PEAR_Downloader_Packages + * @var array + */ + var $_downloadedPackages; + + /** List of file transactions queued for an install/upgrade/uninstall. + * + * Format: + * array( + * 0 => array("rename => array("from-file", "to-file")), + * 1 => array("delete" => array("file-to-delete")), + * ... + * ) + * + * @var array + */ + var $file_operations = array(); + + // }}} + + // {{{ constructor + + /** + * PEAR_Installer constructor. + * + * @param object $ui user interface object (instance of PEAR_Frontend_*) + * + * @access public + */ + function PEAR_Installer(&$ui) + { + parent::PEAR_Common(); + $this->setFrontendObject($ui); + $this->debug = $this->config->get('verbose'); + } + + function setOptions($options) + { + $this->_options = $options; + } + + function setConfig(&$config) + { + $this->config = &$config; + $this->_registry = &$config->getRegistry(); + } + + // }}} + + function _removeBackups($files) + { + foreach ($files as $path) { + $this->addFileOperation('removebackup', array($path)); + } + } + + // {{{ _deletePackageFiles() + + /** + * Delete a package's installed files, does not remove empty directories. + * + * @param string package name + * @param string channel name + * @param bool if true, then files are backed up first + * @return bool TRUE on success, or a PEAR error on failure + * @access protected + */ + function _deletePackageFiles($package, $channel = false, $backup = false) + { + if (!$channel) { + $channel = 'pear.php.net'; + } + if (!strlen($package)) { + return $this->raiseError("No package to uninstall given"); + } + if (strtolower($package) == 'pear' && $channel == 'pear.php.net') { + // to avoid race conditions, include all possible needed files + require_once 'PEAR/Task/Common.php'; + require_once 'PEAR/Task/Replace.php'; + require_once 'PEAR/Task/Unixeol.php'; + require_once 'PEAR/Task/Windowseol.php'; + require_once 'PEAR/PackageFile/v1.php'; + require_once 'PEAR/PackageFile/v2.php'; + require_once 'PEAR/PackageFile/Generator/v1.php'; + require_once 'PEAR/PackageFile/Generator/v2.php'; + } + $filelist = $this->_registry->packageInfo($package, 'filelist', $channel); + if ($filelist == null) { + return $this->raiseError("$channel/$package not installed"); + } + $ret = array(); + foreach ($filelist as $file => $props) { + if (empty($props['installed_as'])) { + continue; + } + $path = $props['installed_as']; + if ($backup) { + $this->addFileOperation('backup', array($path)); + $ret[] = $path; + } + $this->addFileOperation('delete', array($path)); + } + if ($backup) { + return $ret; + } + return true; + } + + // }}} + // {{{ _installFile() + + /** + * @param string filename + * @param array attributes from tag in package.xml + * @param string path to install the file in + * @param array options from command-line + * @access private + */ + function _installFile($file, $atts, $tmp_path, $options) + { + // {{{ return if this file is meant for another platform + static $os; + if (!isset($this->_registry)) { + $this->_registry = &$this->config->getRegistry(); + } + if (isset($atts['platform'])) { + if (empty($os)) { + $os = new OS_Guess(); + } + if (strlen($atts['platform']) && $atts['platform']{0} == '!') { + $negate = true; + $platform = substr($atts['platform'], 1); + } else { + $negate = false; + $platform = $atts['platform']; + } + if ((bool) $os->matchSignature($platform) === $negate) { + $this->log(3, "skipped $file (meant for $atts[platform], we are ".$os->getSignature().")"); + return PEAR_INSTALLER_SKIPPED; + } + } + // }}} + + $channel = $this->pkginfo->getChannel(); + // {{{ assemble the destination paths + switch ($atts['role']) { + case 'doc': + case 'data': + case 'test': + $dest_dir = $this->config->get($atts['role'] . '_dir', null, $channel) . + DIRECTORY_SEPARATOR . $this->pkginfo->getPackage(); + unset($atts['baseinstalldir']); + break; + case 'ext': + case 'php': + $dest_dir = $this->config->get($atts['role'] . '_dir', null, $channel); + break; + case 'script': + $dest_dir = $this->config->get('bin_dir', null, $channel); + break; + case 'src': + case 'extsrc': + $this->source_files++; + return; + default: + return $this->raiseError("Invalid role `$atts[role]' for file $file"); + } + $save_destdir = $dest_dir; + if (!empty($atts['baseinstalldir'])) { + $dest_dir .= DIRECTORY_SEPARATOR . $atts['baseinstalldir']; + } + if (dirname($file) != '.' && empty($atts['install-as'])) { + $dest_dir .= DIRECTORY_SEPARATOR . dirname($file); + } + if (empty($atts['install-as'])) { + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . basename($file); + } else { + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . $atts['install-as']; + } + $orig_file = $tmp_path . DIRECTORY_SEPARATOR . $file; + + // Clean up the DIRECTORY_SEPARATOR mess + $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; + list($dest_file, $orig_file) = preg_replace(array('!\\\\+!', '!/!', "!$ds2+!"), + array(DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR), + array($dest_file, $orig_file)); + $final_dest_file = $installed_as = $dest_file; + if (isset($this->_options['packagingroot'])) { + $installedas_dest_dir = dirname($final_dest_file); + $installedas_dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file); + $final_dest_file = $this->_prependPath($final_dest_file, + $this->_options['packagingroot']); + } else { + $installedas_dest_dir = dirname($final_dest_file); + $installedas_dest_file = $installedas_dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file); + } + $dest_dir = dirname($final_dest_file); + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file); + if (preg_match('~/\.\.(/|\\z)|^\.\./~', str_replace('\\', '/', $dest_file))) { + return $this->raiseError("SECURITY ERROR: file $file (installed to $dest_file) contains parent directory reference ..", PEAR_INSTALLER_FAILED); + } + // }}} + + if (empty($this->_options['register-only']) && + (!file_exists($dest_dir) || !is_dir($dest_dir))) { + if (!$this->mkDirHier($dest_dir)) { + return $this->raiseError("failed to mkdir $dest_dir", + PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ mkdir $dest_dir"); + } + // pretty much nothing happens if we are only registering the install + if (empty($this->_options['register-only'])) { + if (empty($atts['replacements'])) { + if (!file_exists($orig_file)) { + return $this->raiseError("file $orig_file does not exist", + PEAR_INSTALLER_FAILED); + } + if (!@copy($orig_file, $dest_file)) { + return $this->raiseError("failed to write $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ cp $orig_file $dest_file"); + if (isset($atts['md5sum'])) { + $md5sum = md5_file($dest_file); + } + } else { + // {{{ file with replacements + if (!file_exists($orig_file)) { + return $this->raiseError("file does not exist", + PEAR_INSTALLER_FAILED); + } + $contents = file_get_contents($orig_file); + if ($contents === false) { + $contents = ''; + } + if (isset($atts['md5sum'])) { + $md5sum = md5($contents); + } + $subst_from = $subst_to = array(); + foreach ($atts['replacements'] as $a) { + $to = ''; + if ($a['type'] == 'php-const') { + if (preg_match('/^[a-z0-9_]+\\z/i', $a['to'])) { + eval("\$to = $a[to];"); + } else { + if (!isset($options['soft'])) { + $this->log(0, "invalid php-const replacement: $a[to]"); + } + continue; + } + } elseif ($a['type'] == 'pear-config') { + if ($a['to'] == 'master_server') { + $chan = $this->_registry->getChannel($channel); + if (!PEAR::isError($chan)) { + $to = $chan->getServer(); + } else { + $to = $this->config->get($a['to'], null, $channel); + } + } else { + $to = $this->config->get($a['to'], null, $channel); + } + if (is_null($to)) { + if (!isset($options['soft'])) { + $this->log(0, "invalid pear-config replacement: $a[to]"); + } + continue; + } + } elseif ($a['type'] == 'package-info') { + if ($t = $this->pkginfo->packageInfo($a['to'])) { + $to = $t; + } else { + if (!isset($options['soft'])) { + $this->log(0, "invalid package-info replacement: $a[to]"); + } + continue; + } + } + if (!is_null($to)) { + $subst_from[] = $a['from']; + $subst_to[] = $to; + } + } + $this->log(3, "doing ".sizeof($subst_from)." substitution(s) for $final_dest_file"); + if (sizeof($subst_from)) { + $contents = str_replace($subst_from, $subst_to, $contents); + } + $wp = @fopen($dest_file, "wb"); + if (!is_resource($wp)) { + return $this->raiseError("failed to create $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + if (@fwrite($wp, $contents) === false) { + return $this->raiseError("failed writing to $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + fclose($wp); + // }}} + } + // {{{ check the md5 + if (isset($md5sum)) { + if (strtolower($md5sum) == strtolower($atts['md5sum'])) { + $this->log(2, "md5sum ok: $final_dest_file"); + } else { + if (empty($options['force'])) { + // delete the file + if (file_exists($dest_file)) { + unlink($dest_file); + } + if (!isset($options['ignore-errors'])) { + return $this->raiseError("bad md5sum for file $final_dest_file", + PEAR_INSTALLER_FAILED); + } else { + if (!isset($options['soft'])) { + $this->log(0, "warning : bad md5sum for file $final_dest_file"); + } + } + } else { + if (!isset($options['soft'])) { + $this->log(0, "warning : bad md5sum for file $final_dest_file"); + } + } + } + } + // }}} + // {{{ set file permissions + if (!OS_WINDOWS) { + if ($atts['role'] == 'script') { + $mode = 0777 & ~(int)octdec($this->config->get('umask')); + $this->log(3, "+ chmod +x $dest_file"); + } else { + $mode = 0666 & ~(int)octdec($this->config->get('umask')); + } + $this->addFileOperation("chmod", array($mode, $dest_file)); + if (!@chmod($dest_file, $mode)) { + if (!isset($options['soft'])) { + $this->log(0, "failed to change mode of $dest_file: $php_errormsg"); + } + } + } + // }}} + $this->addFileOperation("rename", array($dest_file, $final_dest_file, + $atts['role'] == 'ext')); + } + // Store the full path where the file was installed for easy unistall + $this->addFileOperation("installed_as", array($file, $installed_as, + $save_destdir, dirname(substr($installedas_dest_file, strlen($save_destdir))))); + + //$this->log(2, "installed: $dest_file"); + return PEAR_INSTALLER_OK; + } + + // }}} + // {{{ _installFile2() + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param string filename + * @param array attributes from tag in package.xml + * @param string path to install the file in + * @param array options from command-line + * @access private + */ + function _installFile2(&$pkg, $file, $atts, $tmp_path, $options) + { + if (!isset($this->_registry)) { + $this->_registry = &$this->config->getRegistry(); + } + + $channel = $pkg->getChannel(); + // {{{ assemble the destination paths + if (!in_array($atts['attribs']['role'], + PEAR_Installer_Role::getValidRoles($pkg->getPackageType()))) { + return $this->raiseError('Invalid role `' . $atts['attribs']['role'] . + "' for file $file"); + } + $role = &PEAR_Installer_Role::factory($pkg, $atts['attribs']['role'], $this->config); + $err = $role->setup($this, $pkg, $atts['attribs'], $file); + if (PEAR::isError($err)) { + return $err; + } + if (!$role->isInstallable()) { + return; + } + $info = $role->processInstallation($pkg, $atts['attribs'], $file, $tmp_path); + if (PEAR::isError($info)) { + return $info; + } else { + list($save_destdir, $dest_dir, $dest_file, $orig_file) = $info; + } + if (preg_match('~/\.\.(/|\\z)|^\.\./~', str_replace('\\', '/', $dest_file))) { + return $this->raiseError("SECURITY ERROR: file $file (installed to $dest_file) contains parent directory reference ..", PEAR_INSTALLER_FAILED); + } + $final_dest_file = $installed_as = $dest_file; + if (isset($this->_options['packagingroot'])) { + $final_dest_file = $this->_prependPath($final_dest_file, + $this->_options['packagingroot']); + } + $dest_dir = dirname($final_dest_file); + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file); + // }}} + + if (empty($this->_options['register-only'])) { + if (!file_exists($dest_dir) || !is_dir($dest_dir)) { + if (!$this->mkDirHier($dest_dir)) { + return $this->raiseError("failed to mkdir $dest_dir", + PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ mkdir $dest_dir"); + } + } + $attribs = $atts['attribs']; + unset($atts['attribs']); + // pretty much nothing happens if we are only registering the install + if (empty($this->_options['register-only'])) { + if (!count($atts)) { // no tasks + if (!file_exists($orig_file)) { + return $this->raiseError("file $orig_file does not exist", + PEAR_INSTALLER_FAILED); + } + if (!@copy($orig_file, $dest_file)) { + return $this->raiseError("failed to write $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ cp $orig_file $dest_file"); + if (isset($attribs['md5sum'])) { + $md5sum = md5_file($dest_file); + } + } else { // file with tasks + if (!file_exists($orig_file)) { + return $this->raiseError("file $orig_file does not exist", + PEAR_INSTALLER_FAILED); + } + $contents = file_get_contents($orig_file); + if ($contents === false) { + $contents = ''; + } + if (isset($attribs['md5sum'])) { + $md5sum = md5($contents); + } + foreach ($atts as $tag => $raw) { + $tag = str_replace(array($pkg->getTasksNs() . ':', '-'), + array('', '_'), $tag); + $task = "PEAR_Task_$tag"; + $task = &new $task($this->config, $this, PEAR_TASK_INSTALL); + if (!$task->isScript()) { // scripts are only handled after installation + $task->init($raw, $attribs, $pkg->getLastInstalledVersion()); + $res = $task->startSession($pkg, $contents, $final_dest_file); + if ($res === false) { + continue; // skip this file + } + if (PEAR::isError($res)) { + return $res; + } + $contents = $res; // save changes + } + $wp = @fopen($dest_file, "wb"); + if (!is_resource($wp)) { + return $this->raiseError("failed to create $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + if (fwrite($wp, $contents) === false) { + return $this->raiseError("failed writing to $dest_file: $php_errormsg", + PEAR_INSTALLER_FAILED); + } + fclose($wp); + } + } + // {{{ check the md5 + if (isset($md5sum)) { + if (strtolower($md5sum) == strtolower($attribs['md5sum'])) { + $this->log(2, "md5sum ok: $final_dest_file"); + } else { + if (empty($options['force'])) { + // delete the file + if (file_exists($dest_file)) { + unlink($dest_file); + } + if (!isset($options['ignore-errors'])) { + return $this->raiseError("bad md5sum for file $final_dest_file", + PEAR_INSTALLER_FAILED); + } else { + if (!isset($options['soft'])) { + $this->log(0, "warning : bad md5sum for file $final_dest_file"); + } + } + } else { + if (!isset($options['soft'])) { + $this->log(0, "warning : bad md5sum for file $final_dest_file"); + } + } + } + } + // }}} + // {{{ set file permissions + if (!OS_WINDOWS) { + if ($role->isExecutable()) { + $mode = 0777 & ~(int)octdec($this->config->get('umask')); + $this->log(3, "+ chmod +x $dest_file"); + } else { + $mode = 0666 & ~(int)octdec($this->config->get('umask')); + } + $this->addFileOperation("chmod", array($mode, $dest_file)); + if (!@chmod($dest_file, $mode)) { + if (!isset($options['soft'])) { + $this->log(0, "failed to change mode of $dest_file: $php_errormsg"); + } + } + } + // }}} + $this->addFileOperation("rename", array($dest_file, $final_dest_file, $role->isExtension())); + } + // Store the full path where the file was installed for easy uninstall + $this->addFileOperation("installed_as", array($file, $installed_as, + $save_destdir, dirname(substr($dest_file, strlen($save_destdir))))); + + //$this->log(2, "installed: $dest_file"); + return PEAR_INSTALLER_OK; + } + + // }}} + // {{{ addFileOperation() + + /** + * Add a file operation to the current file transaction. + * + * @see startFileTransaction() + * @param string $type This can be one of: + * - rename: rename a file ($data has 3 values) + * - backup: backup an existing file ($data has 1 value) + * - removebackup: clean up backups created during install ($data has 1 value) + * - chmod: change permissions on a file ($data has 2 values) + * - delete: delete a file ($data has 1 value) + * - rmdir: delete a directory if empty ($data has 1 value) + * - installed_as: mark a file as installed ($data has 4 values). + * @param array $data For all file operations, this array must contain the + * full path to the file or directory that is being operated on. For + * the rename command, the first parameter must be the file to rename, + * the second its new name, the third whether this is a PHP extension. + * + * The installed_as operation contains 4 elements in this order: + * 1. Filename as listed in the filelist element from package.xml + * 2. Full path to the installed file + * 3. Full path from the php_dir configuration variable used in this + * installation + * 4. Relative path from the php_dir that this file is installed in + */ + function addFileOperation($type, $data) + { + if (!is_array($data)) { + return $this->raiseError('Internal Error: $data in addFileOperation' + . ' must be an array, was ' . gettype($data)); + } + if ($type == 'chmod') { + $octmode = decoct($data[0]); + $this->log(3, "adding to transaction: $type $octmode $data[1]"); + } else { + $this->log(3, "adding to transaction: $type " . implode(" ", $data)); + } + $this->file_operations[] = array($type, $data); + } + + // }}} + // {{{ startFileTransaction() + + function startFileTransaction($rollback_in_case = false) + { + if (count($this->file_operations) && $rollback_in_case) { + $this->rollbackFileTransaction(); + } + $this->file_operations = array(); + } + + // }}} + // {{{ commitFileTransaction() + + function commitFileTransaction() + { + $n = count($this->file_operations); + $this->log(2, "about to commit $n file operations"); + // {{{ first, check permissions and such manually + $errors = array(); + foreach ($this->file_operations as $tr) { + list($type, $data) = $tr; + switch ($type) { + case 'rename': + if (!file_exists($data[0])) { + $errors[] = "cannot rename file $data[0], doesn't exist"; + } + // check that dest dir. is writable + if (!is_writable(dirname($data[1]))) { + $errors[] = "permission denied ($type): $data[1]"; + } + break; + case 'chmod': + // check that file is writable + if (!is_writable($data[1])) { + $errors[] = "permission denied ($type): $data[1] " . decoct($data[0]); + } + break; + case 'delete': + if (!file_exists($data[0])) { + $this->log(2, "warning: file $data[0] doesn't exist, can't be deleted"); + } + // check that directory is writable + if (file_exists($data[0])) { + if (!is_writable(dirname($data[0]))) { + $errors[] = "permission denied ($type): $data[0]"; + } else { + // make sure the file to be deleted can be opened for writing + $fp = false; + if (!is_dir($data[0]) && + (!is_writable($data[0]) || !($fp = @fopen($data[0], 'a')))) { + $errors[] = "permission denied ($type): $data[0]"; + } elseif ($fp) { + fclose($fp); + } + } + } + break; + } + + } + // }}} + $m = sizeof($errors); + if ($m > 0) { + foreach ($errors as $error) { + if (!isset($this->_options['soft'])) { + $this->log(1, $error); + } + } + if (!isset($this->_options['ignore-errors'])) { + return false; + } + } + $this->_dirtree = array(); + // {{{ really commit the transaction + foreach ($this->file_operations as $i => $tr) { + if (!$tr) { + // support removal of non-existing backups + continue; + } + list($type, $data) = $tr; + switch ($type) { + case 'backup': + if (!file_exists($data[0])) { + $this->file_operations[$i] = false; + break; + } + if (!@copy($data[0], $data[0] . '.bak')) { + $this->log(1, 'Could not copy ' . $data[0] . ' to ' . $data[0] . + '.bak ' . $php_errormsg); + return false; + } + $this->log(3, "+ backup $data[0] to $data[0].bak"); + break; + case 'removebackup': + if (file_exists($data[0] . '.bak') && is_writable($data[0] . '.bak')) { + unlink($data[0] . '.bak'); + $this->log(3, "+ rm backup of $data[0] ($data[0].bak)"); + } + break; + case 'rename': + if (file_exists($data[1])) { + $test = @unlink($data[1]); + } else { + $test = null; + } + if (!$test && file_exists($data[1])) { + if ($data[2]) { + $extra = ', this extension must be installed manually. Rename to "' . + basename($data[1]) . '"'; + } else { + $extra = ''; + } + if (!isset($this->_options['soft'])) { + $this->log(1, 'Could not delete ' . $data[1] . ', cannot rename ' . + $data[0] . $extra); + } + if (!isset($this->_options['ignore-errors'])) { + return false; + } + } + // permissions issues with rename - copy() is far superior + $perms = @fileperms($data[0]); + if (!@copy($data[0], $data[1])) { + $this->log(1, 'Could not rename ' . $data[0] . ' to ' . $data[1] . + ' ' . $php_errormsg); + return false; + } + // copy over permissions, otherwise they are lost + @chmod($data[1], $perms); + @unlink($data[0]); + $this->log(3, "+ mv $data[0] $data[1]"); + break; + case 'chmod': + if (!@chmod($data[1], $data[0])) { + $this->log(1, 'Could not chmod ' . $data[1] . ' to ' . + decoct($data[0]) . ' ' . $php_errormsg); + return false; + } + $octmode = decoct($data[0]); + $this->log(3, "+ chmod $octmode $data[1]"); + break; + case 'delete': + if (file_exists($data[0])) { + if (!@unlink($data[0])) { + $this->log(1, 'Could not delete ' . $data[0] . ' ' . + $php_errormsg); + return false; + } + $this->log(3, "+ rm $data[0]"); + } + break; + case 'rmdir': + if (file_exists($data[0])) { + do { + $testme = opendir($data[0]); + while (false !== ($entry = readdir($testme))) { + if ($entry == '.' || $entry == '..') { + continue; + } + closedir($testme); + break 2; // this directory is not empty and can't be + // deleted + } + closedir($testme); + if (!@rmdir($data[0])) { + $this->log(1, 'Could not rmdir ' . $data[0] . ' ' . + $php_errormsg); + return false; + } + $this->log(3, "+ rmdir $data[0]"); + } while (false); + } + break; + case 'installed_as': + $this->pkginfo->setInstalledAs($data[0], $data[1]); + if (!isset($this->_dirtree[dirname($data[1])])) { + $this->_dirtree[dirname($data[1])] = true; + $this->pkginfo->setDirtree(dirname($data[1])); + + while(!empty($data[3]) && $data[3] != '/' && $data[3] != '\\' + && $data[3] != '.') { + $this->pkginfo->setDirtree($pp = + $this->_prependPath($data[3], $data[2])); + $this->_dirtree[$pp] = true; + $data[3] = dirname($data[3]); + } + } + break; + } + } + // }}} + $this->log(2, "successfully committed $n file operations"); + $this->file_operations = array(); + return true; + } + + // }}} + // {{{ rollbackFileTransaction() + + function rollbackFileTransaction() + { + $n = count($this->file_operations); + $this->log(2, "rolling back $n file operations"); + foreach ($this->file_operations as $tr) { + list($type, $data) = $tr; + switch ($type) { + case 'backup': + if (file_exists($data[0] . '.bak')) { + if (file_exists($data[0] && is_writable($data[0]))) { + unlink($data[0]); + } + @copy($data[0] . '.bak', $data[0]); + $this->log(3, "+ restore $data[0] from $data[0].bak"); + } + break; + case 'removebackup': + if (file_exists($data[0] . '.bak') && is_writable($data[0] . '.bak')) { + unlink($data[0] . '.bak'); + $this->log(3, "+ rm backup of $data[0] ($data[0].bak)"); + } + break; + case 'rename': + @unlink($data[0]); + $this->log(3, "+ rm $data[0]"); + break; + case 'mkdir': + @rmdir($data[0]); + $this->log(3, "+ rmdir $data[0]"); + break; + case 'chmod': + break; + case 'delete': + break; + case 'installed_as': + $this->pkginfo->setInstalledAs($data[0], false); + break; + } + } + $this->pkginfo->resetDirtree(); + $this->file_operations = array(); + } + + // }}} + // {{{ mkDirHier($dir) + + function mkDirHier($dir) + { + $this->addFileOperation('mkdir', array($dir)); + return parent::mkDirHier($dir); + } + + // }}} + // {{{ download() + + /** + * Download any files and their dependencies, if necessary + * + * @param array a mixed list of package names, local files, or package.xml + * @param PEAR_Config + * @param array options from the command line + * @param array this is the array that will be populated with packages to + * install. Format of each entry: + * + * + * array('pkg' => 'package_name', 'file' => '/path/to/local/file', + * 'info' => array() // parsed package.xml + * ); + * + * @param array this will be populated with any error messages + * @param false private recursion variable + * @param false private recursion variable + * @param false private recursion variable + * @deprecated in favor of PEAR_Downloader + */ + function download($packages, $options, &$config, &$installpackages, + &$errors, $installed = false, $willinstall = false, $state = false) + { + // trickiness: initialize here + parent::PEAR_Downloader($this->ui, $options, $config); + $ret = parent::download($packages); + $errors = $this->getErrorMsgs(); + $installpackages = $this->getDownloadedPackages(); + trigger_error("PEAR Warning: PEAR_Installer::download() is deprecated " . + "in favor of PEAR_Downloader class", E_USER_WARNING); + return $ret; + } + + // }}} + // {{{ _parsePackageXml() + + function _parsePackageXml(&$descfile, &$tmpdir) + { + if (substr($descfile, -4) == '.xml') { + $tmpdir = false; + } else { + // {{{ Decompress pack in tmp dir ------------------------------------- + + // To allow relative package file names + $descfile = realpath($descfile); + + if (PEAR::isError($tmpdir = System::mktemp('-d'))) { + return $tmpdir; + } + $this->log(3, '+ tmp dir created at ' . $tmpdir); + // }}} + } + // Parse xml file ----------------------------------------------- + $pkg = new PEAR_PackageFile($this->config, $this->debug, $tmpdir); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $p = &$pkg->fromAnyFile($descfile, PEAR_VALIDATE_INSTALLING); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($p)) { + if (is_array($p->getUserInfo())) { + foreach ($p->getUserInfo() as $err) { + $loglevel = $err['level'] == 'error' ? 0 : 1; + if (!isset($this->_options['soft'])) { + $this->log($loglevel, ucfirst($err['level']) . ': ' . $err['message']); + } + } + } + return $this->raiseError('Installation failed: invalid package file'); + } else { + $descfile = $p->getPackageFile(); + } + return $p; + } + + // }}} + /** + * Set the list of PEAR_Downloader_Package objects to allow more sane + * dependency validation + * @param array + */ + function setDownloadedPackages(&$pkgs) + { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->analyzeDependencies($pkgs); + PEAR::popErrorHandling(); + if (PEAR::isError($err)) { + return $err; + } + $this->_downloadedPackages = &$pkgs; + } + + /** + * Set the list of PEAR_Downloader_Package objects to allow more sane + * dependency validation + * @param array + */ + function setUninstallPackages(&$pkgs) + { + $this->_downloadedPackages = &$pkgs; + } + + function getInstallPackages() + { + return $this->_downloadedPackages; + } + + // {{{ install() + + /** + * Installs the files within the package file specified. + * + * @param string|PEAR_Downloader_Package $pkgfile path to the package file, + * or a pre-initialized packagefile object + * @param array $options + * recognized options: + * - installroot : optional prefix directory for installation + * - force : force installation + * - register-only : update registry but don't install files + * - upgrade : upgrade existing install + * - soft : fail silently + * - nodeps : ignore dependency conflicts/missing dependencies + * - alldeps : install all dependencies + * - onlyreqdeps : install only required dependencies + * + * @return array|PEAR_Error package info if successful + */ + + function install($pkgfile, $options = array()) + { + $this->_options = $options; + $this->_registry = &$this->config->getRegistry(); + if (is_object($pkgfile)) { + $dlpkg = &$pkgfile; + $pkg = $pkgfile->getPackageFile(); + $pkgfile = $pkg->getArchiveFile(); + $descfile = $pkg->getPackageFile(); + $tmpdir = dirname($descfile); + } else { + $descfile = $pkgfile; + $tmpdir = ''; + if (PEAR::isError($pkg = &$this->_parsePackageXml($descfile, $tmpdir))) { + return $pkg; + } + } + + if (realpath($descfile) != realpath($pkgfile)) { + $tar = new Archive_Tar($pkgfile); + if (!$tar->extract($tmpdir)) { + return $this->raiseError("unable to unpack $pkgfile"); + } + } + + $pkgname = $pkg->getName(); + $channel = $pkg->getChannel(); + if (isset($this->_options['packagingroot'])) { + $regdir = $this->_prependPath( + $this->config->get('php_dir', null, 'pear.php.net'), + $this->_options['packagingroot']); + $packrootphp_dir = $this->_prependPath( + $this->config->get('php_dir', null, $channel), + $this->_options['packagingroot']); + } + + if (isset($options['installroot'])) { + $this->config->setInstallRoot($options['installroot']); + $this->_registry = &$this->config->getRegistry(); + $installregistry = &$this->_registry; + $this->installroot = ''; // all done automagically now + $php_dir = $this->config->get('php_dir', null, $channel); + } else { + $this->config->setInstallRoot(false); + $this->_registry = &$this->config->getRegistry(); + if (isset($this->_options['packagingroot'])) { + $installregistry = &new PEAR_Registry($regdir); + if (!$installregistry->channelExists($channel, true)) { + // we need to fake a channel-discover of this channel + $chanobj = $this->_registry->getChannel($channel, true); + $installregistry->addChannel($chanobj); + } + $php_dir = $packrootphp_dir; + } else { + $installregistry = &$this->_registry; + $php_dir = $this->config->get('php_dir', null, $channel); + } + $this->installroot = ''; + } + + // {{{ checks to do when not in "force" mode + if (empty($options['force']) && + (file_exists($this->config->get('php_dir')) && + is_dir($this->config->get('php_dir')))) { + $testp = $channel == 'pear.php.net' ? $pkgname : array($channel, $pkgname); + $instfilelist = $pkg->getInstallationFileList(true); + if (PEAR::isError($instfilelist)) { + return $instfilelist; + } + // ensure we have the most accurate registry + $installregistry->flushFileMap(); + $test = $installregistry->checkFileMap($instfilelist, $testp, '1.1'); + if (PEAR::isError($test)) { + return $test; + } + if (sizeof($test)) { + $pkgs = $this->getInstallPackages(); + $found = false; + foreach ($pkgs as $param) { + if ($pkg->isSubpackageOf($param)) { + $found = true; + break; + } + } + if ($found) { + // subpackages can conflict with earlier versions of parent packages + $parentreg = $installregistry->packageInfo($param->getPackage(), null, $param->getChannel()); + $tmp = $test; + foreach ($tmp as $file => $info) { + if (is_array($info)) { + if (strtolower($info[1]) == strtolower($param->getPackage()) && + strtolower($info[0]) == strtolower($param->getChannel())) { + unset($test[$file]); + unset($parentreg['filelist'][$file]); + } + } else { + if (strtolower($param->getChannel()) != 'pear.php.net') { + continue; + } + if (strtolower($info) == strtolower($param->getPackage())) { + unset($test[$file]); + unset($parentreg['filelist'][$file]); + } + } + } + $pfk = &new PEAR_PackageFile($this->config); + $parentpkg = &$pfk->fromArray($parentreg); + $installregistry->updatePackage2($parentpkg); + } + if ($param->getChannel() == 'pecl.php.net' && isset($options['upgrade'])) { + $tmp = $test; + foreach ($tmp as $file => $info) { + if (is_string($info)) { + // pear.php.net packages are always stored as strings + if (strtolower($info) == strtolower($param->getPackage())) { + // upgrading existing package + unset($test[$file]); + } + } + } + } + if (sizeof($test)) { + $msg = "$channel/$pkgname: conflicting files found:\n"; + $longest = max(array_map("strlen", array_keys($test))); + $fmt = "%${longest}s (%s)\n"; + foreach ($test as $file => $info) { + if (!is_array($info)) { + $info = array('pear.php.net', $info); + } + $info = $info[0] . '/' . $info[1]; + $msg .= sprintf($fmt, $file, $info); + } + if (!isset($options['ignore-errors'])) { + return $this->raiseError($msg); + } else { + if (!isset($options['soft'])) { + $this->log(0, "WARNING: $msg"); + } + } + } + } + } + // }}} + + $this->startFileTransaction(); + + if (empty($options['upgrade']) && empty($options['soft'])) { + // checks to do only when installing new packages + if ($channel == 'pecl.php.net') { + $test = $installregistry->packageExists($pkgname, $channel); + if (!$test) { + $test = $installregistry->packageExists($pkgname, 'pear.php.net'); + } + } else { + $test = $installregistry->packageExists($pkgname, $channel); + } + if (empty($options['force']) && $test) { + return $this->raiseError("$channel/$pkgname is already installed"); + } + } else { + $usechannel = $channel; + if ($channel == 'pecl.php.net') { + $test = $installregistry->packageExists($pkgname, $channel); + if (!$test) { + $test = $installregistry->packageExists($pkgname, 'pear.php.net'); + $usechannel = 'pear.php.net'; + } + } else { + $test = $installregistry->packageExists($pkgname, $channel); + } + if ($test) { + $v1 = $installregistry->packageInfo($pkgname, 'version', $usechannel); + $v2 = $pkg->getVersion(); + $cmp = version_compare("$v1", "$v2", 'gt'); + if (empty($options['force']) && !version_compare("$v2", "$v1", 'gt')) { + return $this->raiseError("upgrade to a newer version ($v2 is not newer than $v1)"); + } + if (empty($options['register-only'])) { + // when upgrading, remove old release's files first: + if (PEAR::isError($err = $this->_deletePackageFiles($pkgname, $usechannel, + true))) { + if (!isset($options['ignore-errors'])) { + return $this->raiseError($err); + } else { + if (!isset($options['soft'])) { + $this->log(0, 'WARNING: ' . $err->getMessage()); + } + } + } else { + $backedup = $err; + } + } + } + } + + // {{{ Copy files to dest dir --------------------------------------- + + // info from the package it self we want to access from _installFile + $this->pkginfo = &$pkg; + // used to determine whether we should build any C code + $this->source_files = 0; + + $savechannel = $this->config->get('default_channel'); + if (empty($options['register-only']) && !is_dir($php_dir)) { + if (PEAR::isError(System::mkdir(array('-p'), $php_dir))) { + return $this->raiseError("no installation destination directory '$php_dir'\n"); + } + } + + $tmp_path = dirname($descfile); + if (substr($pkgfile, -4) != '.xml') { + $tmp_path .= DIRECTORY_SEPARATOR . $pkgname . '-' . $pkg->getVersion(); + } + + $this->configSet('default_channel', $channel); + // {{{ install files + + $ver = $pkg->getPackagexmlVersion(); + if (version_compare($ver, '2.0', '>=')) { + $filelist = $pkg->getInstallationFilelist(); + } else { + $filelist = $pkg->getFileList(); + } + if (PEAR::isError($filelist)) { + return $filelist; + } + $pkg->resetFilelist(); + $pkg->setLastInstalledVersion($installregistry->packageInfo($pkg->getPackage(), + 'version', $pkg->getChannel())); + foreach ($filelist as $file => $atts) { + if ($pkg->getPackagexmlVersion() == '1.0') { + $this->expectError(PEAR_INSTALLER_FAILED); + $res = $this->_installFile($file, $atts, $tmp_path, $options); + $this->popExpect(); + } else { + $this->expectError(PEAR_INSTALLER_FAILED); + $res = $this->_installFile2($pkg, $file, $atts, $tmp_path, $options); + $this->popExpect(); + } + if (PEAR::isError($res)) { + if (empty($options['ignore-errors'])) { + $this->rollbackFileTransaction(); + if ($res->getMessage() == "file does not exist") { + $this->raiseError("file $file in package.xml does not exist"); + } + return $this->raiseError($res); + } else { + if (!isset($options['soft'])) { + $this->log(0, "Warning: " . $res->getMessage()); + } + } + } + if ($res == PEAR_INSTALLER_OK) { + // Register files that were installed + $pkg->installedFile($file, $atts); + } + } + // }}} + + // {{{ compile and install source files + if ($this->source_files > 0 && empty($options['nobuild'])) { + if (PEAR::isError($err = + $this->_compileSourceFiles($savechannel, $pkg))) { + return $err; + } + } + // }}} + + if (isset($backedup)) { + $this->_removeBackups($backedup); + } + if (!$this->commitFileTransaction()) { + $this->rollbackFileTransaction(); + $this->configSet('default_channel', $savechannel); + return $this->raiseError("commit failed", PEAR_INSTALLER_FAILED); + } + // }}} + + $ret = false; + $installphase = 'install'; + $oldversion = false; + // {{{ Register that the package is installed ----------------------- + if (empty($options['upgrade'])) { + // if 'force' is used, replace the info in registry + $usechannel = $channel; + if ($channel == 'pecl.php.net') { + $test = $installregistry->packageExists($pkgname, $channel); + if (!$test) { + $test = $installregistry->packageExists($pkgname, 'pear.php.net'); + $usechannel = 'pear.php.net'; + } + } else { + $test = $installregistry->packageExists($pkgname, $channel); + } + if (!empty($options['force']) && $test) { + $oldversion = $installregistry->packageInfo($pkgname, 'version', $usechannel); + $installregistry->deletePackage($pkgname, $usechannel); + } + $ret = $installregistry->addPackage2($pkg); + } else { + $usechannel = $channel; + if ($channel == 'pecl.php.net') { + $test = $installregistry->packageExists($pkgname, $channel); + if (!$test) { + $test = $installregistry->packageExists($pkgname, 'pear.php.net'); + $usechannel = 'pear.php.net'; + } + } else { + $test = $installregistry->packageExists($pkgname, $channel); + } + // new: upgrade installs a package if it isn't installed + if (!$test) { + $ret = $installregistry->addPackage2($pkg); + } else { + if ($usechannel != $channel) { + $installregistry->deletePackage($pkgname, $usechannel); + $ret = $installregistry->addPackage2($pkg); + } else { + $ret = $installregistry->updatePackage2($pkg); + } + $installphase = 'upgrade'; + } + } + if (!$ret) { + $this->configSet('default_channel', $savechannel); + return $this->raiseError("Adding package $channel/$pkgname to registry failed"); + } + // }}} + $this->configSet('default_channel', $savechannel); + if (class_exists('PEAR_Task_Common')) { // this is auto-included if any tasks exist + if (PEAR_Task_Common::hasPostinstallTasks()) { + PEAR_Task_Common::runPostinstallTasks($installphase); + } + } + return $pkg->toArray(true); + } + + // }}} + + // {{{ _compileSourceFiles() + /** + * @param string + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + */ + function _compileSourceFiles($savechannel, &$filelist) + { + require_once 'PEAR/Builder.php'; + $this->log(1, "$this->source_files source files, building"); + $bob = &new PEAR_Builder($this->ui); + $bob->debug = $this->debug; + $built = $bob->build($filelist, array(&$this, '_buildCallback')); + if (PEAR::isError($built)) { + $this->rollbackFileTransaction(); + $this->configSet('default_channel', $savechannel); + return $built; + } + $this->log(1, "\nBuild process completed successfully"); + foreach ($built as $ext) { + $bn = basename($ext['file']); + list($_ext_name, $_ext_suff) = explode('.', $bn); + if ($_ext_suff == '.so' || $_ext_suff == '.dll') { + if (extension_loaded($_ext_name)) { + $this->raiseError("Extension '$_ext_name' already loaded. " . + 'Please unload it in your php.ini file ' . + 'prior to install or upgrade'); + } + $role = 'ext'; + } else { + $role = 'src'; + } + $dest = $ext['dest']; + $packagingroot = ''; + if (isset($this->_options['packagingroot'])) { + $packagingroot = $this->_options['packagingroot']; + } + $copyto = $this->_prependPath($dest, $packagingroot); + if ($copyto != $dest) { + $this->log(1, "Installing '$dest' as '$copyto'"); + } else { + $this->log(1, "Installing '$dest'"); + } + $copydir = dirname($copyto); + // pretty much nothing happens if we are only registering the install + if (empty($this->_options['register-only'])) { + if (!file_exists($copydir) || !is_dir($copydir)) { + if (!$this->mkDirHier($copydir)) { + return $this->raiseError("failed to mkdir $copydir", + PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ mkdir $copydir"); + } + if (!@copy($ext['file'], $copyto)) { + return $this->raiseError("failed to write $copyto ($php_errormsg)", PEAR_INSTALLER_FAILED); + } + $this->log(3, "+ cp $ext[file] $copyto"); + $this->addFileOperation('rename', array($ext['file'], $copyto)); + if (!OS_WINDOWS) { + $mode = 0666 & ~(int)octdec($this->config->get('umask')); + $this->addFileOperation('chmod', array($mode, $copyto)); + if (!@chmod($copyto, $mode)) { + $this->log(0, "failed to change mode of $copyto ($php_errormsg)"); + } + } + } + + if ($filelist->getPackageXmlVersion() == '1.0') { + $filelist->installedFile($bn, array( + 'role' => $role, + 'name' => $bn, + 'installed_as' => $dest, + 'php_api' => $ext['php_api'], + 'zend_mod_api' => $ext['zend_mod_api'], + 'zend_ext_api' => $ext['zend_ext_api'], + )); + } else { + $filelist->installedFile($bn, array('attribs' => array( + 'role' => $role, + 'name' => $bn, + 'installed_as' => $dest, + 'php_api' => $ext['php_api'], + 'zend_mod_api' => $ext['zend_mod_api'], + 'zend_ext_api' => $ext['zend_ext_api'], + ))); + } + } + } + + // }}} + function &getUninstallPackages() + { + return $this->_downloadedPackages; + } + // {{{ uninstall() + + /** + * Uninstall a package + * + * This method removes all files installed by the application, and then + * removes any empty directories. + * @param string package name + * @param array Command-line options. Possibilities include: + * + * - installroot: base installation dir, if not the default + * - register-only : update registry but don't remove files + * - nodeps: do not process dependencies of other packages to ensure + * uninstallation does not break things + */ + function uninstall($package, $options = array()) + { + if (isset($options['installroot'])) { + $this->config->setInstallRoot($options['installroot']); + $this->installroot = ''; + } else { + $this->config->setInstallRoot(''); + $this->installroot = ''; + } + $this->_registry = &$this->config->getRegistry(); + if (is_object($package)) { + $channel = $package->getChannel(); + $pkg = $package; + $package = $pkg->getPackage(); + } else { + $pkg = false; + $info = $this->_registry->parsePackageName($package, + $this->config->get('default_channel')); + $channel = $info['channel']; + $package = $info['package']; + } + $savechannel = $this->config->get('default_channel'); + $this->configSet('default_channel', $channel); + if (!is_object($pkg)) { + $pkg = $this->_registry->getPackage($package, $channel); + } + if (!$pkg) { + $this->configSet('default_channel', $savechannel); + return $this->raiseError($this->_registry->parsedPackageNameToString( + array( + 'channel' => $channel, + 'package' => $package + ), true) . ' not installed'); + } + if ($pkg->getInstalledBinary()) { + // this is just an alias for a binary package + return $this->_registry->deletePackage($package, $channel); + } + $filelist = $pkg->getFilelist(); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + $depchecker = &new PEAR_Dependency2($this->config, $options, + array('channel' => $channel, 'package' => $package), + PEAR_VALIDATE_UNINSTALLING); + $e = $depchecker->validatePackageUninstall($this); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($e)) { + if (!isset($options['ignore-errors'])) { + return $this->raiseError($e); + } else { + if (!isset($options['soft'])) { + $this->log(0, 'WARNING: ' . $e->getMessage()); + } + } + } elseif (is_array($e)) { + if (!isset($options['soft'])) { + $this->log(0, $e[0]); + } + } + $this->pkginfo = &$pkg; + // pretty much nothing happens if we are only registering the uninstall + if (empty($options['register-only'])) { + // {{{ Delete the files + $this->startFileTransaction(); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + if (PEAR::isError($err = $this->_deletePackageFiles($package, $channel))) { + PEAR::popErrorHandling(); + $this->rollbackFileTransaction(); + $this->configSet('default_channel', $savechannel); + if (!isset($options['ignore-errors'])) { + return $this->raiseError($err); + } else { + if (!isset($options['soft'])) { + $this->log(0, 'WARNING: ' . $err->getMessage()); + } + } + } else { + PEAR::popErrorHandling(); + } + if (!$this->commitFileTransaction()) { + $this->rollbackFileTransaction(); + if (!isset($options['ignore-errors'])) { + return $this->raiseError("uninstall failed"); + } elseif (!isset($options['soft'])) { + $this->log(0, 'WARNING: uninstall failed'); + } + } else { + $this->startFileTransaction(); + if ($dirtree = $pkg->getDirTree()) { + // attempt to delete empty directories + uksort($dirtree, array($this, '_sortDirs')); + foreach($dirtree as $dir => $notused) { + $this->addFileOperation('rmdir', array($dir)); + } + } else { + $this->configSet('default_channel', $savechannel); + return $this->_registry->deletePackage($package, $channel); + } + if (!$this->commitFileTransaction()) { + $this->rollbackFileTransaction(); + if (!isset($options['ignore-errors'])) { + return $this->raiseError("uninstall failed"); + } elseif (!isset($options['soft'])) { + $this->log(0, 'WARNING: uninstall failed'); + } + } + } + // }}} + } + + $this->configSet('default_channel', $savechannel); + // Register that the package is no longer installed + return $this->_registry->deletePackage($package, $channel); + } + + /** + * Sort a list of arrays of array(downloaded packagefilename) by dependency. + * + * It also removes duplicate dependencies + * @param array an array of PEAR_PackageFile_v[1/2] objects + * @return array|PEAR_Error array of array(packagefilename, package.xml contents) + */ + function sortPackagesForUninstall(&$packages) + { + $this->_dependencyDB = &PEAR_DependencyDB::singleton($this->config); + if (PEAR::isError($this->_dependencyDB)) { + return $this->_dependencyDB; + } + usort($packages, array(&$this, '_sortUninstall')); + } + + function _sortUninstall($a, $b) + { + if (!$a->getDeps() && !$b->getDeps()) { + return 0; // neither package has dependencies, order is insignificant + } + if ($a->getDeps() && !$b->getDeps()) { + return -1; // $a must be installed after $b because $a has dependencies + } + if (!$a->getDeps() && $b->getDeps()) { + return 1; // $b must be installed after $a because $b has dependencies + } + // both packages have dependencies + if ($this->_dependencyDB->dependsOn($a, $b)) { + return -1; + } + if ($this->_dependencyDB->dependsOn($b, $a)) { + return 1; + } + return 0; + } + + // }}} + // {{{ _sortDirs() + function _sortDirs($a, $b) + { + if (strnatcmp($a, $b) == -1) return 1; + if (strnatcmp($a, $b) == 1) return -1; + return 0; + } + + // }}} + + // {{{ _buildCallback() + + function _buildCallback($what, $data) + { + if (($what == 'cmdoutput' && $this->debug > 1) || + ($what == 'output' && $this->debug > 0)) { + $this->ui->outputData(rtrim($data), 'build'); + } + } + + // }}} +} + +// {{{ md5_file() utility function +if (!function_exists("md5_file")) { + function md5_file($filename) { + if (!$fd = @fopen($file, 'r')) { + return false; + } + fclose($fd); + return md5(file_get_contents($filename)); + } +} +// }}} + +?> \ No newline at end of file diff --git a/PEAR/Installer/Role.php b/PEAR/Installer/Role.php new file mode 100644 index 0000000..dabac22 --- /dev/null +++ b/PEAR/Installer/Role.php @@ -0,0 +1,253 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Role.php,v 1.16 2006/10/31 02:54:41 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * base class for installer roles + */ +require_once 'PEAR/Installer/Role/Common.php'; +require_once 'PEAR/XMLParser.php'; +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role +{ + /** + * Set up any additional configuration variables that file roles require + * + * Never call this directly, it is called by the PEAR_Config constructor + * @param PEAR_Config + * @access private + * @static + */ + function initializeConfig(&$config) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + foreach ($GLOBALS['_PEAR_INSTALLER_ROLES'] as $class => $info) { + if (!$info['config_vars']) { + continue; + } + $config->_addConfigVars($info['config_vars']); + } + } + + /** + * @param PEAR_PackageFile_v2 + * @param string role name + * @param PEAR_Config + * @return PEAR_Installer_Role_Common + * @static + */ + function &factory($pkg, $role, &$config) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + if (!in_array($role, PEAR_Installer_Role::getValidRoles($pkg->getPackageType()))) { + $a = false; + return $a; + } + $a = 'PEAR_Installer_Role_' . ucfirst($role); + if (!class_exists($a)) { + require_once str_replace('_', '/', $a) . '.php'; + } + $b = new $a($config); + return $b; + } + + /** + * Get a list of file roles that are valid for the particular release type. + * + * For instance, src files serve no purpose in regular php releases. + * @param string + * @param bool clear cache + * @return array + * @static + */ + function getValidRoles($release, $clear = false) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + static $ret = array(); + if ($clear) { + $ret = array(); + } + if (isset($ret[$release])) { + return $ret[$release]; + } + $ret[$release] = array(); + foreach ($GLOBALS['_PEAR_INSTALLER_ROLES'] as $role => $okreleases) { + if (in_array($release, $okreleases['releasetypes'])) { + $ret[$release][] = strtolower(str_replace('PEAR_Installer_Role_', '', $role)); + } + } + return $ret[$release]; + } + + /** + * Get a list of roles that require their files to be installed + * + * Most roles must be installed, but src and package roles, for instance + * are pseudo-roles. src files are compiled into a new extension. Package + * roles are actually fully bundled releases of a package + * @param bool clear cache + * @return array + * @static + */ + function getInstallableRoles($clear = false) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + static $ret; + if ($clear) { + unset($ret); + } + if (!isset($ret)) { + $ret = array(); + foreach ($GLOBALS['_PEAR_INSTALLER_ROLES'] as $role => $okreleases) { + if ($okreleases['installable']) { + $ret[] = strtolower(str_replace('PEAR_Installer_Role_', '', $role)); + } + } + } + return $ret; + } + + /** + * Return an array of roles that are affected by the baseinstalldir attribute + * + * Most roles ignore this attribute, and instead install directly into: + * PackageName/filepath + * so a tests file tests/file.phpt is installed into PackageName/tests/filepath.php + * @param bool clear cache + * @return array + * @static + */ + function getBaseinstallRoles($clear = false) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + static $ret; + if ($clear) { + unset($ret); + } + if (!isset($ret)) { + $ret = array(); + foreach ($GLOBALS['_PEAR_INSTALLER_ROLES'] as $role => $okreleases) { + if ($okreleases['honorsbaseinstall']) { + $ret[] = strtolower(str_replace('PEAR_Installer_Role_', '', $role)); + } + } + } + return $ret; + } + + /** + * Return an array of file roles that should be analyzed for PHP content at package time, + * like the "php" role. + * @param bool clear cache + * @return array + * @static + */ + function getPhpRoles($clear = false) + { + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'])) { + PEAR_Installer_Role::registerRoles(); + } + static $ret; + if ($clear) { + unset($ret); + } + if (!isset($ret)) { + $ret = array(); + foreach ($GLOBALS['_PEAR_INSTALLER_ROLES'] as $role => $okreleases) { + if ($okreleases['phpfile']) { + $ret[] = strtolower(str_replace('PEAR_Installer_Role_', '', $role)); + } + } + } + return $ret; + } + + /** + * Scan through the Command directory looking for classes + * and see what commands they implement. + * @param string which directory to look for classes, defaults to + * the Installer/Roles subdirectory of + * the directory from where this file (__FILE__) is + * included. + * + * @return bool TRUE on success, a PEAR error on failure + * @access public + * @static + */ + function registerRoles($dir = null) + { + $GLOBALS['_PEAR_INSTALLER_ROLES'] = array(); + $parser = new PEAR_XMLParser; + if ($dir === null) { + $dir = dirname(__FILE__) . '/Role'; + } + if (!file_exists($dir) || !is_dir($dir)) { + return PEAR::raiseError("registerRoles: opendir($dir) failed"); + } + $dp = @opendir($dir); + if (empty($dp)) { + return PEAR::raiseError("registerRoles: opendir($dir) failed"); + } + while ($entry = readdir($dp)) { + if ($entry{0} == '.' || substr($entry, -4) != '.xml') { + continue; + } + $class = "PEAR_Installer_Role_".substr($entry, 0, -4); + // List of roles + if (!isset($GLOBALS['_PEAR_INSTALLER_ROLES'][$class])) { + $file = "$dir/$entry"; + $parser->parse(file_get_contents($file)); + $data = $parser->getData(); + if (!is_array($data['releasetypes'])) { + $data['releasetypes'] = array($data['releasetypes']); + } + $GLOBALS['_PEAR_INSTALLER_ROLES'][$class] = $data; + } + } + closedir($dp); + ksort($GLOBALS['_PEAR_INSTALLER_ROLES']); + PEAR_Installer_Role::getBaseinstallRoles(true); + PEAR_Installer_Role::getInstallableRoles(true); + PEAR_Installer_Role::getPhpRoles(true); + PEAR_Installer_Role::getValidRoles('****', true); + return true; + } +} +?> diff --git a/PEAR/Installer/Role/Common.php b/PEAR/Installer/Role/Common.php new file mode 100644 index 0000000..48811bd --- /dev/null +++ b/PEAR/Installer/Role/Common.php @@ -0,0 +1,180 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Common.php,v 1.12 2006/10/19 23:55:32 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Base class for all installation roles. + * + * This class allows extensibility of file roles. Packages with complex + * customization can now provide custom file roles along with the possibility of + * adding configuration values to match. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Common +{ + /** + * @var PEAR_Config + * @access protected + */ + var $config; + + /** + * @param PEAR_Config + */ + function PEAR_Installer_Role_Common(&$config) + { + $this->config = $config; + } + + /** + * Retrieve configuration information about a file role from its XML info + * + * @param string $role Role Classname, as in "PEAR_Installer_Role_Data" + * @return array + */ + function getInfo($role) + { + if (empty($GLOBALS['_PEAR_INSTALLER_ROLES'][$role])) { + return PEAR::raiseError('Unknown Role class: "' . $role . '"'); + } + return $GLOBALS['_PEAR_INSTALLER_ROLES'][$role]; + } + + /** + * This is called for each file to set up the directories and files + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param array attributes from the tag + * @param string file name + * @return array an array consisting of: + * + * 1 the original, pre-baseinstalldir installation directory + * 2 the final installation directory + * 3 the full path to the final location of the file + * 4 the location of the pre-installation file + */ + function processInstallation($pkg, $atts, $file, $tmp_path, $layer = null) + { + $roleInfo = PEAR_Installer_Role_Common::getInfo('PEAR_Installer_Role_' . + ucfirst(str_replace('pear_installer_role_', '', strtolower(get_class($this))))); + if (PEAR::isError($roleInfo)) { + return $roleInfo; + } + if (!$roleInfo['locationconfig']) { + return false; + } + if ($roleInfo['honorsbaseinstall']) { + $dest_dir = $save_destdir = $this->config->get($roleInfo['locationconfig'], $layer, + $pkg->getChannel()); + if (!empty($atts['baseinstalldir'])) { + $dest_dir .= DIRECTORY_SEPARATOR . $atts['baseinstalldir']; + } + } elseif ($roleInfo['unusualbaseinstall']) { + $dest_dir = $save_destdir = $this->config->get($roleInfo['locationconfig'], + $layer, $pkg->getChannel()) . DIRECTORY_SEPARATOR . $pkg->getPackage(); + if (!empty($atts['baseinstalldir'])) { + $dest_dir .= DIRECTORY_SEPARATOR . $atts['baseinstalldir']; + } + } else { + $dest_dir = $save_destdir = $this->config->get($roleInfo['locationconfig'], + $layer, $pkg->getChannel()) . DIRECTORY_SEPARATOR . $pkg->getPackage(); + } + if (dirname($file) != '.' && empty($atts['install-as'])) { + $dest_dir .= DIRECTORY_SEPARATOR . dirname($file); + } + if (empty($atts['install-as'])) { + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . basename($file); + } else { + $dest_file = $dest_dir . DIRECTORY_SEPARATOR . $atts['install-as']; + } + $orig_file = $tmp_path . DIRECTORY_SEPARATOR . $file; + + // Clean up the DIRECTORY_SEPARATOR mess + $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; + + list($dest_dir, $dest_file, $orig_file) = preg_replace(array('!\\\\+!', '!/!', "!$ds2+!"), + array(DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR), + array($dest_dir, $dest_file, $orig_file)); + return array($save_destdir, $dest_dir, $dest_file, $orig_file); + } + + /** + * Get the name of the configuration variable that specifies the location of this file + * @return string|false + */ + function getLocationConfig() + { + $roleInfo = PEAR_Installer_Role_Common::getInfo('PEAR_Installer_Role_' . + ucfirst(str_replace('pear_installer_role_', '', strtolower(get_class($this))))); + if (PEAR::isError($roleInfo)) { + return $roleInfo; + } + return $roleInfo['locationconfig']; + } + + /** + * Do any unusual setup here + * @param PEAR_Installer + * @param PEAR_PackageFile_v2 + * @param array file attributes + * @param string file name + */ + function setup(&$installer, $pkg, $atts, $file) + { + } + + function isExecutable() + { + $roleInfo = PEAR_Installer_Role_Common::getInfo('PEAR_Installer_Role_' . + ucfirst(str_replace('pear_installer_role_', '', strtolower(get_class($this))))); + if (PEAR::isError($roleInfo)) { + return $roleInfo; + } + return $roleInfo['executable']; + } + + function isInstallable() + { + $roleInfo = PEAR_Installer_Role_Common::getInfo('PEAR_Installer_Role_' . + ucfirst(str_replace('pear_installer_role_', '', strtolower(get_class($this))))); + if (PEAR::isError($roleInfo)) { + return $roleInfo; + } + return $roleInfo['installable']; + } + + function isExtension() + { + $roleInfo = PEAR_Installer_Role_Common::getInfo('PEAR_Installer_Role_' . + ucfirst(str_replace('pear_installer_role_', '', strtolower(get_class($this))))); + if (PEAR::isError($roleInfo)) { + return $roleInfo; + } + return $roleInfo['phpextension']; + } +} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Data.php b/PEAR/Installer/Role/Data.php new file mode 100644 index 0000000..c010257 --- /dev/null +++ b/PEAR/Installer/Role/Data.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Data.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Data extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Data.xml b/PEAR/Installer/Role/Data.xml new file mode 100644 index 0000000..eae6372 --- /dev/null +++ b/PEAR/Installer/Role/Data.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + data_dir + + + + + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Doc.php b/PEAR/Installer/Role/Doc.php new file mode 100644 index 0000000..2bdb358 --- /dev/null +++ b/PEAR/Installer/Role/Doc.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Doc.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Doc extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Doc.xml b/PEAR/Installer/Role/Doc.xml new file mode 100644 index 0000000..173afba --- /dev/null +++ b/PEAR/Installer/Role/Doc.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + doc_dir + + + + + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Ext.php b/PEAR/Installer/Role/Ext.php new file mode 100644 index 0000000..c8386cd --- /dev/null +++ b/PEAR/Installer/Role/Ext.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Ext.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Ext extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Ext.xml b/PEAR/Installer/Role/Ext.xml new file mode 100644 index 0000000..e2940fe --- /dev/null +++ b/PEAR/Installer/Role/Ext.xml @@ -0,0 +1,12 @@ + + extbin + zendextbin + 1 + ext_dir + 1 + + + + 1 + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Php.php b/PEAR/Installer/Role/Php.php new file mode 100644 index 0000000..aba109d --- /dev/null +++ b/PEAR/Installer/Role/Php.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Php.php,v 1.7 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Php extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Php.xml b/PEAR/Installer/Role/Php.xml new file mode 100644 index 0000000..6b9a0e6 --- /dev/null +++ b/PEAR/Installer/Role/Php.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + php_dir + 1 + + 1 + + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Script.php b/PEAR/Installer/Role/Script.php new file mode 100644 index 0000000..f55f5b4 --- /dev/null +++ b/PEAR/Installer/Role/Script.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Script.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Script extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Script.xml b/PEAR/Installer/Role/Script.xml new file mode 100644 index 0000000..e732cf2 --- /dev/null +++ b/PEAR/Installer/Role/Script.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + bin_dir + 1 + + + 1 + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Src.php b/PEAR/Installer/Role/Src.php new file mode 100644 index 0000000..fe0187f --- /dev/null +++ b/PEAR/Installer/Role/Src.php @@ -0,0 +1,40 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Src.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Src extends PEAR_Installer_Role_Common +{ + function setup(&$installer, $pkg, $atts, $file) + { + $installer->source_files++; + } +} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Src.xml b/PEAR/Installer/Role/Src.xml new file mode 100644 index 0000000..ebe439a --- /dev/null +++ b/PEAR/Installer/Role/Src.xml @@ -0,0 +1,12 @@ + + extsrc + zendextsrc + + + + + + + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Test.php b/PEAR/Installer/Role/Test.php new file mode 100644 index 0000000..c64e469 --- /dev/null +++ b/PEAR/Installer/Role/Test.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Test.php,v 1.6 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Installer_Role_Test extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Test.xml b/PEAR/Installer/Role/Test.xml new file mode 100644 index 0000000..51d5b89 --- /dev/null +++ b/PEAR/Installer/Role/Test.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + test_dir + + + + + + + \ No newline at end of file diff --git a/PEAR/PackageFile.php b/PEAR/PackageFile.php new file mode 100644 index 0000000..76e6ead --- /dev/null +++ b/PEAR/PackageFile.php @@ -0,0 +1,474 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: PackageFile.php,v 1.40 2006/09/25 05:12:21 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * needed for PEAR_VALIDATE_* constants + */ +require_once 'PEAR/Validate.php'; +/** + * Error code if the package.xml tag does not contain a valid version + */ +define('PEAR_PACKAGEFILE_ERROR_NO_PACKAGEVERSION', 1); +/** + * Error code if the package.xml tag version is not supported (version 1.0 and 1.1 are the only supported versions, + * currently + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_PACKAGEVERSION', 2); +/** + * Abstraction for the package.xml package description file + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile +{ + /** + * @var PEAR_Config + */ + var $_config; + var $_debug; + /** + * Temp directory for uncompressing tgz files. + * @var string|false + */ + var $_tmpdir; + var $_logger = false; + /** + * @var boolean + */ + var $_rawReturn = false; + + /** + * + * @param PEAR_Config $config + * @param ? $debug + * @param string @tmpdir Optional temporary directory for uncompressing + * files + */ + function PEAR_PackageFile(&$config, $debug = false, $tmpdir = false) + { + $this->_config = $config; + $this->_debug = $debug; + $this->_tmpdir = $tmpdir; + } + + /** + * Turn off validation - return a parsed package.xml without checking it + * + * This is used by the package-validate command + */ + function rawReturn() + { + $this->_rawReturn = true; + } + + function setLogger(&$l) + { + $this->_logger = &$l; + } + + /** + * Create a PEAR_PackageFile_Parser_v* of a given version. + * @param int $version + * @return PEAR_PackageFile_Parser_v1|PEAR_PackageFile_Parser_v1 + */ + function &parserFactory($version) + { + if (!in_array($version{0}, array('1', '2'))) { + $a = false; + return $a; + } + include_once 'PEAR/PackageFile/Parser/v' . $version{0} . '.php'; + $version = $version{0}; + $class = "PEAR_PackageFile_Parser_v$version"; + $a = new $class; + return $a; + } + + /** + * For simpler unit-testing + * @return string + */ + function getClassPrefix() + { + return 'PEAR_PackageFile_v'; + } + + /** + * Create a PEAR_PackageFile_v* of a given version. + * @param int $version + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v1 + */ + function &factory($version) + { + if (!in_array($version{0}, array('1', '2'))) { + $a = false; + return $a; + } + include_once 'PEAR/PackageFile/v' . $version{0} . '.php'; + $version = $version{0}; + $class = $this->getClassPrefix() . $version; + $a = new $class; + return $a; + } + + /** + * Create a PEAR_PackageFile_v* from its toArray() method + * + * WARNING: no validation is performed, the array is assumed to be valid, + * always parse from xml if you want validation. + * @param array $arr + * @return PEAR_PackageFileManager_v1|PEAR_PackageFileManager_v2 + * @uses factory() to construct the returned object. + */ + function &fromArray($arr) + { + if (isset($arr['xsdversion'])) { + $obj = &$this->factory($arr['xsdversion']); + if ($this->_logger) { + $obj->setLogger($this->_logger); + } + $obj->setConfig($this->_config); + $obj->fromArray($arr); + return $obj; + } else { + if (isset($arr['package']['attribs']['version'])) { + $obj = &$this->factory($arr['package']['attribs']['version']); + } else { + $obj = &$this->factory('1.0'); + } + if ($this->_logger) { + $obj->setLogger($this->_logger); + } + $obj->setConfig($this->_config); + $obj->fromArray($arr); + return $obj; + } + } + + /** + * Create a PEAR_PackageFile_v* from an XML string. + * @access public + * @param string $data contents of package.xml file + * @param int $state package state (one of PEAR_VALIDATE_* constants) + * @param string $file full path to the package.xml file (and the files + * it references) + * @param string $archive optional name of the archive that the XML was + * extracted from, if any + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @uses parserFactory() to construct a parser to load the package. + */ + function &fromXmlString($data, $state, $file, $archive = false) + { + if (preg_match('/]+version="([0-9]+\.[0-9]+)"/', $data, $packageversion)) { + if (!in_array($packageversion[1], array('1.0', '2.0', '2.1'))) { + return PEAR::raiseError('package.xml version "' . $packageversion[1] . + '" is not supported, only 1.0, 2.0, and 2.1 are supported.'); + } + $object = &$this->parserFactory($packageversion[1]); + if ($this->_logger) { + $object->setLogger($this->_logger); + } + $object->setConfig($this->_config); + $pf = $object->parse($data, $file, $archive); + if (PEAR::isError($pf)) { + return $pf; + } + if ($this->_rawReturn) { + return $pf; + } + if ($pf->validate($state)) { + if ($this->_logger) { + if ($pf->getValidationWarnings(false)) { + foreach ($pf->getValidationWarnings() as $warning) { + $this->_logger->log(0, 'WARNING: ' . $warning['message']); + } + } + } + if (method_exists($pf, 'flattenFilelist')) { + $pf->flattenFilelist(); // for v2 + } + return $pf; + } else { + if ($this->_config->get('verbose') > 0) { + if ($this->_logger) { + if ($pf->getValidationWarnings(false)) { + foreach ($pf->getValidationWarnings(false) as $warning) { + $this->_logger->log(0, 'ERROR: ' . $warning['message']); + } + } + } + } + $a = PEAR::raiseError('Parsing of package.xml from file "' . $file . '" failed', + 2, null, null, $pf->getValidationWarnings()); + return $a; + } + } elseif (preg_match('/]+version="([^"]+)"/', $data, $packageversion)) { + $a = PEAR::raiseError('package.xml file "' . $file . + '" has unsupported package.xml version "' . $packageversion[1] . '"'); + return $a; + } else { + if (!class_exists('PEAR_ErrorStack')) { + require_once 'PEAR/ErrorStack.php'; + } + PEAR_ErrorStack::staticPush('PEAR_PackageFile', + PEAR_PACKAGEFILE_ERROR_NO_PACKAGEVERSION, + 'warning', array('xml' => $data), 'package.xml "' . $file . + '" has no package.xml version'); + $object = &$this->parserFactory('1.0'); + $object->setConfig($this->_config); + $pf = $object->parse($data, $file, $archive); + if (PEAR::isError($pf)) { + return $pf; + } + if ($this->_rawReturn) { + return $pf; + } + if ($pf->validate($state)) { + if ($this->_logger) { + if ($pf->getValidationWarnings(false)) { + foreach ($pf->getValidationWarnings() as $warning) { + $this->_logger->log(0, 'WARNING: ' . $warning['message']); + } + } + } + if (method_exists($pf, 'flattenFilelist')) { + $pf->flattenFilelist(); // for v2 + } + return $pf; + } else { + $a = PEAR::raiseError('Parsing of package.xml from file "' . $file . '" failed', + 2, null, null, $pf->getValidationWarnings()); + return $a; + } + } + } + + /** + * Register a temporary file or directory. When the destructor is + * executed, all registered temporary files and directories are + * removed. + * + * @param string $file name of file or directory + * @return void + */ + function addTempFile($file) + { + $GLOBALS['_PEAR_Common_tempfiles'][] = $file; + } + + /** + * Create a PEAR_PackageFile_v* from a compresed Tar or Tgz file. + * @access public + * @param string contents of package.xml file + * @param int package state (one of PEAR_VALIDATE_* constants) + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @using Archive_Tar to extract the files + * @using fromPackageFile() to load the package after the package.xml + * file is extracted. + */ + function &fromTgzFile($file, $state) + { + if (!class_exists('Archive_Tar')) { + require_once 'Archive/Tar.php'; + } + $tar = new Archive_Tar($file); + if ($this->_debug <= 1) { + $tar->pushErrorHandling(PEAR_ERROR_RETURN); + } + $content = $tar->listContent(); + if ($this->_debug <= 1) { + $tar->popErrorHandling(); + } + if (!is_array($content)) { + if (is_string($file) && strlen($file < 255) && + (!file_exists($file) || !@is_file($file))) { + $ret = PEAR::raiseError("could not open file \"$file\""); + return $ret; + } + $file = realpath($file); + $ret = PEAR::raiseError("Could not get contents of package \"$file\"". + '. Invalid tgz file.'); + return $ret; + } else { + if (!count($content) && !@is_file($file)) { + $ret = PEAR::raiseError("could not open file \"$file\""); + return $ret; + } + } + $xml = null; + $origfile = $file; + foreach ($content as $file) { + $name = $file['filename']; + if ($name == 'package2.xml') { // allow a .tgz to distribute both versions + $xml = $name; + break; + } + if ($name == 'package.xml') { + $xml = $name; + break; + } elseif (ereg('package.xml$', $name, $match)) { + $xml = $name; + break; + } + } + if ($this->_tmpdir) { + $tmpdir = $this->_tmpdir; + } else { + $tmpdir = System::mkTemp(array('-d', 'pear')); + PEAR_PackageFile::addTempFile($tmpdir); + } + $this->_extractErrors(); + PEAR::staticPushErrorHandling(PEAR_ERROR_CALLBACK, array($this, '_extractErrors')); + if (!$xml || !$tar->extractList(array($xml), $tmpdir)) { + $extra = implode("\n", $this->_extractErrors()); + if ($extra) { + $extra = ' ' . $extra; + } + PEAR::staticPopErrorHandling(); + $ret = PEAR::raiseError('could not extract the package.xml file from "' . + $origfile . '"' . $extra); + return $ret; + } + PEAR::staticPopErrorHandling(); + $ret = &PEAR_PackageFile::fromPackageFile("$tmpdir/$xml", $state, $origfile); + return $ret; + } + + /** + * helper for extracting Archive_Tar errors + * @var array + * @access private + */ + var $_extractErrors = array(); + + /** + * helper callback for extracting Archive_Tar errors + * + * @param PEAR_Error|null $err + * @return array + * @access private + */ + function _extractErrors($err = null) + { + static $errors = array(); + if ($err === null) { + $e = $errors; + $errors = array(); + return $e; + } + $errors[] = $err->getMessage(); + } + + /** + * Create a PEAR_PackageFile_v* from a package.xml file. + * + * @access public + * @param string $descfile name of package xml file + * @param int $state package state (one of PEAR_VALIDATE_* constants) + * @param string|false $archive name of the archive this package.xml came + * from, if any + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @uses PEAR_PackageFile::fromXmlString to create the oject after the + * XML is loaded from the package.xml file. + */ + function &fromPackageFile($descfile, $state, $archive = false) + { + if (is_string($descfile) && strlen($descfile) < 255 && + (!file_exists($descfile) || !is_file($descfile) || !is_readable($descfile) || + (!$fp = @fopen($descfile, 'r')))) { + $a = PEAR::raiseError("Unable to open $descfile"); + return $a; + } + + // read the whole thing so we only get one cdata callback + // for each block of cdata + fclose($fp); + $data = file_get_contents($descfile); + $ret = &PEAR_PackageFile::fromXmlString($data, $state, $descfile, $archive); + return $ret; + } + + + /** + * Create a PEAR_PackageFile_v* from a .tgz archive or package.xml file. + * + * This method is able to extract information about a package from a .tgz + * archive or from a XML package definition file. + * + * @access public + * @param string $info file name + * @param int $state package state (one of PEAR_VALIDATE_* constants) + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @uses fromPackageFile() if the file appears to be XML + * @uses fromTgzFile() to load all non-XML files + */ + function &fromAnyFile($info, $state) + { + if (is_dir($info)) { + $dir_name = realpath($info); + if (file_exists($dir_name . '/package.xml')) { + $info = PEAR_PackageFile::fromPackageFile($dir_name . '/package.xml', $state); + } elseif (file_exists($dir_name . '/package2.xml')) { + $info = PEAR_PackageFile::fromPackageFile($dir_name . '/package2.xml', $state); + } else { + $info = PEAR::raiseError("No package definition found in '$info' directory"); + } + return $info; + } + + $fp = false; + if (is_string($info) && strlen($info) < 255 && + (file_exists($info) || ($fp = @fopen($info, 'r')))) { + if ($fp) { + fclose($fp); + } + $tmp = substr($info, -4); + if ($tmp == '.xml') { + $info = &PEAR_PackageFile::fromPackageFile($info, $state); + } elseif ($tmp == '.tar' || $tmp == '.tgz') { + $info = &PEAR_PackageFile::fromTgzFile($info, $state); + } else { + $fp = fopen($info, "r"); + $test = fread($fp, 5); + fclose($fp); + if ($test == " diff --git a/PEAR/PackageFile/Generator/v1.php b/PEAR/PackageFile/Generator/v1.php new file mode 100644 index 0000000..dde5e2c --- /dev/null +++ b/PEAR/PackageFile/Generator/v1.php @@ -0,0 +1,1272 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v1.php,v 1.72 2006/05/10 02:56:19 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * needed for PEAR_VALIDATE_* constants + */ +require_once 'PEAR/Validate.php'; +require_once 'System.php'; +require_once 'PEAR/PackageFile/v2.php'; +/** + * This class converts a PEAR_PackageFile_v1 object into any output format. + * + * Supported output formats include array, XML string, and a PEAR_PackageFile_v2 + * object, for converting package.xml 1.0 into package.xml 2.0 with no sweat. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_Generator_v1 +{ + /** + * @var PEAR_PackageFile_v1 + */ + var $_packagefile; + function PEAR_PackageFile_Generator_v1(&$packagefile) + { + $this->_packagefile = &$packagefile; + } + + function getPackagerVersion() + { + return '1.6.1'; + } + + /** + * @param PEAR_Packager + * @param bool if true, a .tgz is written, otherwise a .tar is written + * @param string|null directory in which to save the .tgz + * @return string|PEAR_Error location of package or error object + */ + function toTgz(&$packager, $compress = true, $where = null) + { + require_once 'Archive/Tar.php'; + if ($where === null) { + if (!($where = System::mktemp(array('-d')))) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: mktemp failed'); + } + } elseif (!@System::mkDir(array('-p', $where))) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: "' . $where . '" could' . + ' not be created'); + } + if (file_exists($where . DIRECTORY_SEPARATOR . 'package.xml') && + !is_file($where . DIRECTORY_SEPARATOR . 'package.xml')) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: unable to save package.xml as' . + ' "' . $where . DIRECTORY_SEPARATOR . 'package.xml"'); + } + if (!$this->_packagefile->validate(PEAR_VALIDATE_PACKAGING)) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: invalid package file'); + } + $pkginfo = $this->_packagefile->getArray(); + $ext = $compress ? '.tgz' : '.tar'; + $pkgver = $pkginfo['package'] . '-' . $pkginfo['version']; + $dest_package = getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext; + if (file_exists(getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext) && + !is_file(getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext)) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: cannot create tgz file "' . + getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext . '"'); + } + if ($pkgfile = $this->_packagefile->getPackageFile()) { + $pkgdir = dirname(realpath($pkgfile)); + $pkgfile = basename($pkgfile); + } else { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: package file object must ' . + 'be created from a real file'); + } + // {{{ Create the package file list + $filelist = array(); + $i = 0; + + foreach ($this->_packagefile->getFilelist() as $fname => $atts) { + $file = $pkgdir . DIRECTORY_SEPARATOR . $fname; + if (!file_exists($file)) { + return PEAR::raiseError("File does not exist: $fname"); + } else { + $filelist[$i++] = $file; + if (!isset($atts['md5sum'])) { + $this->_packagefile->setFileAttribute($fname, 'md5sum', md5_file($file)); + } + $packager->log(2, "Adding file $fname"); + } + } + // }}} + $packagexml = $this->toPackageFile($where, PEAR_VALIDATE_PACKAGING, 'package.xml', true); + if ($packagexml) { + $tar =& new Archive_Tar($dest_package, $compress); + $tar->setErrorHandling(PEAR_ERROR_RETURN); // XXX Don't print errors + // ----- Creates with the package.xml file + $ok = $tar->createModify(array($packagexml), '', $where); + if (PEAR::isError($ok)) { + return $ok; + } elseif (!$ok) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: tarball creation failed'); + } + // ----- Add the content of the package + if (!$tar->addModify($filelist, $pkgver, $pkgdir)) { + return PEAR::raiseError('PEAR_Packagefile_v1::toTgz: tarball creation failed'); + } + return $dest_package; + } + } + + /** + * @param string|null directory to place the package.xml in, or null for a temporary dir + * @param int one of the PEAR_VALIDATE_* constants + * @param string name of the generated file + * @param bool if true, then no analysis will be performed on role="php" files + * @return string|PEAR_Error path to the created file on success + */ + function toPackageFile($where = null, $state = PEAR_VALIDATE_NORMAL, $name = 'package.xml', + $nofilechecking = false) + { + if (!$this->_packagefile->validate($state, $nofilechecking)) { + return PEAR::raiseError('PEAR_Packagefile_v1::toPackageFile: invalid package.xml', + null, null, null, $this->_packagefile->getValidationWarnings()); + } + if ($where === null) { + if (!($where = System::mktemp(array('-d')))) { + return PEAR::raiseError('PEAR_Packagefile_v1::toPackageFile: mktemp failed'); + } + } elseif (!@System::mkDir(array('-p', $where))) { + return PEAR::raiseError('PEAR_Packagefile_v1::toPackageFile: "' . $where . '" could' . + ' not be created'); + } + $newpkgfile = $where . DIRECTORY_SEPARATOR . $name; + $np = @fopen($newpkgfile, 'wb'); + if (!$np) { + return PEAR::raiseError('PEAR_Packagefile_v1::toPackageFile: unable to save ' . + "$name as $newpkgfile"); + } + fwrite($np, $this->toXml($state, true)); + fclose($np); + return $newpkgfile; + } + + /** + * fix both XML encoding to be UTF8, and replace standard XML entities < > " & ' + * + * @param string $string + * @return string + * @access private + */ + function _fixXmlEncoding($string) + { + if (version_compare(phpversion(), '5.0.0', 'lt')) { + $string = utf8_encode($string); + } + return strtr($string, array( + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + '\'' => ''' )); + } + + /** + * Return an XML document based on the package info (as returned + * by the PEAR_Common::infoFrom* methods). + * + * @return string XML data + */ + function toXml($state = PEAR_VALIDATE_NORMAL, $nofilevalidation = false) + { + $this->_packagefile->setDate(date('Y-m-d')); + if (!$this->_packagefile->validate($state, $nofilevalidation)) { + return false; + } + $pkginfo = $this->_packagefile->getArray(); + static $maint_map = array( + "handle" => "user", + "name" => "name", + "email" => "email", + "role" => "role", + ); + $ret = "\n"; + $ret .= "\n"; + $ret .= "\n" . +" $pkginfo[package]"; + if (isset($pkginfo['extends'])) { + $ret .= "\n$pkginfo[extends]"; + } + $ret .= + "\n ".$this->_fixXmlEncoding($pkginfo['summary'])."\n" . +" ".trim($this->_fixXmlEncoding($pkginfo['description']))."\n \n" . +" \n"; + foreach ($pkginfo['maintainers'] as $maint) { + $ret .= " \n"; + foreach ($maint_map as $idx => $elm) { + $ret .= " <$elm>"; + $ret .= $this->_fixXmlEncoding($maint[$idx]); + $ret .= "\n"; + } + $ret .= " \n"; + } + $ret .= " \n"; + $ret .= $this->_makeReleaseXml($pkginfo, false, $state); + if (isset($pkginfo['changelog']) && count($pkginfo['changelog']) > 0) { + $ret .= " \n"; + foreach ($pkginfo['changelog'] as $oldrelease) { + $ret .= $this->_makeReleaseXml($oldrelease, true); + } + $ret .= " \n"; + } + $ret .= "\n"; + return $ret; + } + + // }}} + // {{{ _makeReleaseXml() + + /** + * Generate part of an XML description with release information. + * + * @param array $pkginfo array with release information + * @param bool $changelog whether the result will be in a changelog element + * + * @return string XML data + * + * @access private + */ + function _makeReleaseXml($pkginfo, $changelog = false, $state = PEAR_VALIDATE_NORMAL) + { + // XXX QUOTE ENTITIES IN PCDATA, OR EMBED IN CDATA BLOCKS!! + $indent = $changelog ? " " : ""; + $ret = "$indent \n"; + if (!empty($pkginfo['version'])) { + $ret .= "$indent $pkginfo[version]\n"; + } + if (!empty($pkginfo['release_date'])) { + $ret .= "$indent $pkginfo[release_date]\n"; + } + if (!empty($pkginfo['release_license'])) { + $ret .= "$indent $pkginfo[release_license]\n"; + } + if (!empty($pkginfo['release_state'])) { + $ret .= "$indent $pkginfo[release_state]\n"; + } + if (!empty($pkginfo['release_notes'])) { + $ret .= "$indent ".trim($this->_fixXmlEncoding($pkginfo['release_notes'])) + ."\n$indent \n"; + } + if (!empty($pkginfo['release_warnings'])) { + $ret .= "$indent ".$this->_fixXmlEncoding($pkginfo['release_warnings'])."\n"; + } + if (isset($pkginfo['release_deps']) && sizeof($pkginfo['release_deps']) > 0) { + $ret .= "$indent \n"; + foreach ($pkginfo['release_deps'] as $dep) { + $ret .= "$indent _fixXmlEncoding($c['name']) . "\""; + if (isset($c['default'])) { + $ret .= " default=\"" . $this->_fixXmlEncoding($c['default']) . "\""; + } + $ret .= " prompt=\"" . $this->_fixXmlEncoding($c['prompt']) . "\""; + $ret .= "/>\n"; + } + $ret .= "$indent \n"; + } + if (isset($pkginfo['provides'])) { + foreach ($pkginfo['provides'] as $key => $what) { + $ret .= "$indent recursiveXmlFilelist($pkginfo['filelist']); + } else { + foreach ($pkginfo['filelist'] as $file => $fa) { + if (!isset($fa['role'])) { + $fa['role'] = ''; + } + $ret .= "$indent _fixXmlEncoding($fa['baseinstalldir']) . '"'; + } + if (isset($fa['md5sum'])) { + $ret .= " md5sum=\"$fa[md5sum]\""; + } + if (isset($fa['platform'])) { + $ret .= " platform=\"$fa[platform]\""; + } + if (!empty($fa['install-as'])) { + $ret .= ' install-as="' . + $this->_fixXmlEncoding($fa['install-as']) . '"'; + } + $ret .= ' name="' . $this->_fixXmlEncoding($file) . '"'; + if (empty($fa['replacements'])) { + $ret .= "/>\n"; + } else { + $ret .= ">\n"; + foreach ($fa['replacements'] as $r) { + $ret .= "$indent $v) { + $ret .= " $k=\"" . $this->_fixXmlEncoding($v) .'"'; + } + $ret .= "/>\n"; + } + $ret .= "$indent \n"; + } + } + } + $ret .= "$indent \n"; + } + $ret .= "$indent \n"; + return $ret; + } + + /** + * @param array + * @access protected + */ + function recursiveXmlFilelist($list) + { + $this->_dirs = array(); + foreach ($list as $file => $attributes) { + $this->_addDir($this->_dirs, explode('/', dirname($file)), $file, $attributes); + } + return $this->_formatDir($this->_dirs); + } + + /** + * @param array + * @param array + * @param string|null + * @param array|null + * @access private + */ + function _addDir(&$dirs, $dir, $file = null, $attributes = null) + { + if ($dir == array() || $dir == array('.')) { + $dirs['files'][basename($file)] = $attributes; + return; + } + $curdir = array_shift($dir); + if (!isset($dirs['dirs'][$curdir])) { + $dirs['dirs'][$curdir] = array(); + } + $this->_addDir($dirs['dirs'][$curdir], $dir, $file, $attributes); + } + + /** + * @param array + * @param string + * @param string + * @access private + */ + function _formatDir($dirs, $indent = '', $curdir = '') + { + $ret = ''; + if (!count($dirs)) { + return ''; + } + if (isset($dirs['dirs'])) { + uksort($dirs['dirs'], 'strnatcasecmp'); + foreach ($dirs['dirs'] as $dir => $contents) { + $usedir = "$curdir/$dir"; + $ret .= "$indent \n"; + $ret .= $this->_formatDir($contents, "$indent ", $usedir); + $ret .= "$indent \n"; + } + } + if (isset($dirs['files'])) { + uksort($dirs['files'], 'strnatcasecmp'); + foreach ($dirs['files'] as $file => $attribs) { + $ret .= $this->_formatFile($file, $attribs, $indent); + } + } + return $ret; + } + + /** + * @param string + * @param array + * @param string + * @access private + */ + function _formatFile($file, $attributes, $indent) + { + $ret = "$indent _fixXmlEncoding($attributes['baseinstalldir']) . '"'; + } + if (isset($attributes['md5sum'])) { + $ret .= " md5sum=\"$attributes[md5sum]\""; + } + if (isset($attributes['platform'])) { + $ret .= " platform=\"$attributes[platform]\""; + } + if (!empty($attributes['install-as'])) { + $ret .= ' install-as="' . + $this->_fixXmlEncoding($attributes['install-as']) . '"'; + } + $ret .= ' name="' . $this->_fixXmlEncoding($file) . '"'; + if (empty($attributes['replacements'])) { + $ret .= "/>\n"; + } else { + $ret .= ">\n"; + foreach ($attributes['replacements'] as $r) { + $ret .= "$indent $v) { + $ret .= " $k=\"" . $this->_fixXmlEncoding($v) .'"'; + } + $ret .= "/>\n"; + } + $ret .= "$indent \n"; + } + return $ret; + } + + // {{{ _unIndent() + + /** + * Unindent given string (?) + * + * @param string $str The string that has to be unindented. + * @return string + * @access private + */ + function _unIndent($str) + { + // remove leading newlines + $str = preg_replace('/^[\r\n]+/', '', $str); + // find whitespace at the beginning of the first line + $indent_len = strspn($str, " \t"); + $indent = substr($str, 0, $indent_len); + $data = ''; + // remove the same amount of whitespace from following lines + foreach (explode("\n", $str) as $line) { + if (substr($line, 0, $indent_len) == $indent) { + $data .= substr($line, $indent_len) . "\n"; + } + } + return $data; + } + + /** + * @return array + */ + function dependenciesToV2() + { + $arr = array(); + $this->_convertDependencies2_0($arr); + return $arr['dependencies']; + } + + /** + * Convert a package.xml version 1.0 into version 2.0 + * + * Note that this does a basic conversion, to allow more advanced + * features like bundles and multiple releases + * @param string the classname to instantiate and return. This must be + * PEAR_PackageFile_v2 or a descendant + * @param boolean if true, only valid, deterministic package.xml 1.0 as defined by the + * strictest parameters will be converted + * @return PEAR_PackageFile_v2|PEAR_Error + */ + function &toV2($class = 'PEAR_PackageFile_v2', $strict = false) + { + if ($strict) { + if (!$this->_packagefile->validate()) { + $a = PEAR::raiseError('invalid package.xml version 1.0 cannot be converted' . + ' to version 2.0', null, null, null, + $this->_packagefile->getValidationWarnings(true)); + return $a; + } + } + $arr = array( + 'attribs' => array( + '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\n" . +"http://pear.php.net/dtd/tasks-1.0.xsd\n" . +"http://pear.php.net/dtd/package-2.0\n" . +'http://pear.php.net/dtd/package-2.0.xsd', + ), + 'name' => $this->_packagefile->getPackage(), + 'channel' => 'pear.php.net', + ); + $arr['summary'] = $this->_packagefile->getSummary(); + $arr['description'] = $this->_packagefile->getDescription(); + $maintainers = $this->_packagefile->getMaintainers(); + foreach ($maintainers as $maintainer) { + if ($maintainer['role'] != 'lead') { + continue; + } + $new = array( + 'name' => $maintainer['name'], + 'user' => $maintainer['handle'], + 'email' => $maintainer['email'], + 'active' => 'yes', + ); + $arr['lead'][] = $new; + } + if (!isset($arr['lead'])) { // some people... you know? + $arr['lead'] = array( + 'name' => 'unknown', + 'user' => 'unknown', + 'email' => 'noleadmaintainer@example.com', + 'active' => 'no', + ); + } + if (count($arr['lead']) == 1) { + $arr['lead'] = $arr['lead'][0]; + } + foreach ($maintainers as $maintainer) { + if ($maintainer['role'] == 'lead') { + continue; + } + $new = array( + 'name' => $maintainer['name'], + 'user' => $maintainer['handle'], + 'email' => $maintainer['email'], + 'active' => 'yes', + ); + $arr[$maintainer['role']][] = $new; + } + if (isset($arr['developer']) && count($arr['developer']) == 1) { + $arr['developer'] = $arr['developer'][0]; + } + if (isset($arr['contributor']) && count($arr['contributor']) == 1) { + $arr['contributor'] = $arr['contributor'][0]; + } + if (isset($arr['helper']) && count($arr['helper']) == 1) { + $arr['helper'] = $arr['helper'][0]; + } + $arr['date'] = $this->_packagefile->getDate(); + $arr['version'] = + array( + 'release' => $this->_packagefile->getVersion(), + 'api' => $this->_packagefile->getVersion(), + ); + $arr['stability'] = + array( + 'release' => $this->_packagefile->getState(), + 'api' => $this->_packagefile->getState(), + ); + $licensemap = + array( + 'php' => 'http://www.php.net/license', + 'php license' => 'http://www.php.net/license', + 'lgpl' => 'http://www.gnu.org/copyleft/lesser.html', + 'bsd' => 'http://www.opensource.org/licenses/bsd-license.php', + 'bsd style' => 'http://www.opensource.org/licenses/bsd-license.php', + 'bsd-style' => 'http://www.opensource.org/licenses/bsd-license.php', + 'mit' => 'http://www.opensource.org/licenses/mit-license.php', + 'gpl' => 'http://www.gnu.org/copyleft/gpl.html', + 'apache' => 'http://www.opensource.org/licenses/apache2.0.php' + ); + if (isset($licensemap[strtolower($this->_packagefile->getLicense())])) { + $arr['license'] = array( + 'attribs' => array('uri' => + $licensemap[strtolower($this->_packagefile->getLicense())]), + '_content' => $this->_packagefile->getLicense() + ); + } else { + // don't use bogus uri + $arr['license'] = $this->_packagefile->getLicense(); + } + $arr['notes'] = $this->_packagefile->getNotes(); + $temp = array(); + $arr['contents'] = $this->_convertFilelist2_0($temp); + $this->_convertDependencies2_0($arr); + $release = ($this->_packagefile->getConfigureOptions() || $this->_isExtension) ? + 'extsrcrelease' : 'phprelease'; + if ($release == 'extsrcrelease') { + $arr['channel'] = 'pecl.php.net'; + $arr['providesextension'] = $arr['name']; // assumption + } + $arr[$release] = array(); + if ($this->_packagefile->getConfigureOptions()) { + $arr[$release]['configureoption'] = $this->_packagefile->getConfigureOptions(); + foreach ($arr[$release]['configureoption'] as $i => $opt) { + $arr[$release]['configureoption'][$i] = array('attribs' => $opt); + } + if (count($arr[$release]['configureoption']) == 1) { + $arr[$release]['configureoption'] = $arr[$release]['configureoption'][0]; + } + } + $this->_convertRelease2_0($arr[$release], $temp); + if ($release == 'extsrcrelease' && count($arr[$release]) > 1) { + // multiple extsrcrelease tags added in PEAR 1.4.1 + $arr['dependencies']['required']['pearinstaller']['min'] = '1.4.1'; + } + if ($cl = $this->_packagefile->getChangelog()) { + foreach ($cl as $release) { + $rel = array(); + $rel['version'] = + array( + 'release' => $release['version'], + 'api' => $release['version'], + ); + if (!isset($release['release_state'])) { + $release['release_state'] = 'stable'; + } + $rel['stability'] = + array( + 'release' => $release['release_state'], + 'api' => $release['release_state'], + ); + if (isset($release['release_date'])) { + $rel['date'] = $release['release_date']; + } else { + $rel['date'] = date('Y-m-d'); + } + if (isset($release['release_license'])) { + if (isset($licensemap[strtolower($release['release_license'])])) { + $uri = $licensemap[strtolower($release['release_license'])]; + } else { + $uri = 'http://www.example.com'; + } + $rel['license'] = array( + 'attribs' => array('uri' => $uri), + '_content' => $release['release_license'] + ); + } else { + $rel['license'] = $arr['license']; + } + if (!isset($release['release_notes'])) { + $release['release_notes'] = 'no release notes'; + } + $rel['notes'] = $release['release_notes']; + $arr['changelog']['release'][] = $rel; + } + } + $ret = new $class; + $ret->setConfig($this->_packagefile->_config); + if (isset($this->_packagefile->_logger) && is_object($this->_packagefile->_logger)) { + $ret->setLogger($this->_packagefile->_logger); + } + $ret->fromArray($arr); + return $ret; + } + + /** + * @param array + * @param bool + * @access private + */ + function _convertDependencies2_0(&$release, $internal = false) + { + $peardep = array('pearinstaller' => + array('min' => '1.4.0b1')); // this is a lot safer + $required = $optional = array(); + $release['dependencies'] = array(); + if ($this->_packagefile->hasDeps()) { + foreach ($this->_packagefile->getDeps() as $dep) { + if (!isset($dep['optional']) || $dep['optional'] == 'no') { + $required[] = $dep; + } else { + $optional[] = $dep; + } + } + foreach (array('required', 'optional') as $arr) { + $deps = array(); + foreach ($$arr as $dep) { + // organize deps by dependency type and name + if (!isset($deps[$dep['type']])) { + $deps[$dep['type']] = array(); + } + if (isset($dep['name'])) { + $deps[$dep['type']][$dep['name']][] = $dep; + } else { + $deps[$dep['type']][] = $dep; + } + } + do { + if (isset($deps['php'])) { + $php = array(); + if (count($deps['php']) > 1) { + $php = $this->_processPhpDeps($deps['php']); + } else { + if (!isset($deps['php'][0])) { + list($key, $blah) = each ($deps['php']); // stupid buggy versions + $deps['php'] = array($blah[0]); + } + $php = $this->_processDep($deps['php'][0]); + if (!$php) { + break; // poor mans throw + } + } + $release['dependencies'][$arr]['php'] = $php; + } + } while (false); + do { + if (isset($deps['pkg'])) { + $pkg = array(); + $pkg = $this->_processMultipleDepsName($deps['pkg']); + if (!$pkg) { + break; // poor mans throw + } + $release['dependencies'][$arr]['package'] = $pkg; + } + } while (false); + do { + if (isset($deps['ext'])) { + $pkg = array(); + $pkg = $this->_processMultipleDepsName($deps['ext']); + $release['dependencies'][$arr]['extension'] = $pkg; + } + } while (false); + // skip sapi - it's not supported so nobody will have used it + // skip os - it's not supported in 1.0 + } + } + if (isset($release['dependencies']['required'])) { + $release['dependencies']['required'] = + array_merge($peardep, $release['dependencies']['required']); + } else { + $release['dependencies']['required'] = $peardep; + } + if (!isset($release['dependencies']['required']['php'])) { + $release['dependencies']['required']['php'] = + array('min' => '4.0.0'); + } + $order = array(); + $bewm = $release['dependencies']['required']; + $order['php'] = $bewm['php']; + $order['pearinstaller'] = $bewm['pearinstaller']; + isset($bewm['package']) ? $order['package'] = $bewm['package'] :0; + isset($bewm['extension']) ? $order['extension'] = $bewm['extension'] :0; + $release['dependencies']['required'] = $order; + } + + /** + * @param array + * @access private + */ + function _convertFilelist2_0(&$package) + { + $ret = array('dir' => + array( + 'attribs' => array('name' => '/'), + 'file' => array() + ) + ); + $package['platform'] = + $package['install-as'] = array(); + $this->_isExtension = false; + foreach ($this->_packagefile->getFilelist() as $name => $file) { + $file['name'] = $name; + if (isset($file['role']) && $file['role'] == 'src') { + $this->_isExtension = true; + } + if (isset($file['replacements'])) { + $repl = $file['replacements']; + unset($file['replacements']); + } else { + unset($repl); + } + if (isset($file['install-as'])) { + $package['install-as'][$name] = $file['install-as']; + unset($file['install-as']); + } + if (isset($file['platform'])) { + $package['platform'][$name] = $file['platform']; + unset($file['platform']); + } + $file = array('attribs' => $file); + if (isset($repl)) { + foreach ($repl as $replace ) { + $file['tasks:replace'][] = array('attribs' => $replace); + } + if (count($repl) == 1) { + $file['tasks:replace'] = $file['tasks:replace'][0]; + } + } + $ret['dir']['file'][] = $file; + } + return $ret; + } + + /** + * Post-process special files with install-as/platform attributes and + * make the release tag. + * + * This complex method follows this work-flow to create the release tags: + * + *
+     * - if any install-as/platform exist, create a generic release and fill it with
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     * - create a release for each platform encountered and fill with
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     *   o  tags for 
+     * 
+ * + * It does this by accessing the $package parameter, which contains an array with + * indices: + * + * - platform: mapping of file => OS the file should be installed on + * - install-as: mapping of file => installed name + * - osmap: mapping of OS => list of files that should be installed + * on that OS + * - notosmap: mapping of OS => list of files that should not be + * installed on that OS + * + * @param array + * @param array + * @access private + */ + function _convertRelease2_0(&$release, $package) + { + //- if any install-as/platform exist, create a generic release and fill it with + if (count($package['platform']) || count($package['install-as'])) { + $generic = array(); + $genericIgnore = array(); + foreach ($package['install-as'] as $file => $as) { + //o tags for + if (!isset($package['platform'][$file])) { + $generic[] = $file; + continue; + } + //o tags for + if (isset($package['platform'][$file]) && + $package['platform'][$file]{0} == '!') { + $generic[] = $file; + continue; + } + //o tags for + if (isset($package['platform'][$file]) && + $package['platform'][$file]{0} != '!') { + $genericIgnore[] = $file; + continue; + } + } + foreach ($package['platform'] as $file => $platform) { + if (isset($package['install-as'][$file])) { + continue; + } + if ($platform{0} != '!') { + //o tags for + $genericIgnore[] = $file; + } + } + if (count($package['platform'])) { + $oses = $notplatform = $platform = array(); + foreach ($package['platform'] as $file => $os) { + // get a list of oses + if ($os{0} == '!') { + if (isset($oses[substr($os, 1)])) { + continue; + } + $oses[substr($os, 1)] = count($oses); + } else { + if (isset($oses[$os])) { + continue; + } + $oses[$os] = count($oses); + } + } + //- create a release for each platform encountered and fill with + foreach ($oses as $os => $releaseNum) { + $release[$releaseNum]['installconditions']['os']['name'] = $os; + $release[$releaseNum]['filelist'] = array('install' => array(), + 'ignore' => array()); + foreach ($package['install-as'] as $file => $as) { + //o tags for + if (!isset($package['platform'][$file])) { + $release[$releaseNum]['filelist']['install'][] = + array( + 'attribs' => array( + 'name' => $file, + 'as' => $as, + ), + ); + continue; + } + //o tags for + // + if (isset($package['platform'][$file]) && + $package['platform'][$file] == $os) { + $release[$releaseNum]['filelist']['install'][] = + array( + 'attribs' => array( + 'name' => $file, + 'as' => $as, + ), + ); + continue; + } + //o tags for + // + if (isset($package['platform'][$file]) && + $package['platform'][$file] != "!$os" && + $package['platform'][$file]{0} == '!') { + $release[$releaseNum]['filelist']['install'][] = + array( + 'attribs' => array( + 'name' => $file, + 'as' => $as, + ), + ); + continue; + } + //o tags for + // + if (isset($package['platform'][$file]) && + $package['platform'][$file] == "!$os") { + $release[$releaseNum]['filelist']['ignore'][] = + array( + 'attribs' => array( + 'name' => $file, + ), + ); + continue; + } + //o tags for + // + if (isset($package['platform'][$file]) && + $package['platform'][$file]{0} != '!' && + $package['platform'][$file] != $os) { + $release[$releaseNum]['filelist']['ignore'][] = + array( + 'attribs' => array( + 'name' => $file, + ), + ); + continue; + } + } + foreach ($package['platform'] as $file => $platform) { + if (isset($package['install-as'][$file])) { + continue; + } + //o tags for + if ($platform == "!$os") { + $release[$releaseNum]['filelist']['ignore'][] = + array( + 'attribs' => array( + 'name' => $file, + ), + ); + continue; + } + //o tags for + if ($platform{0} != '!' && $platform != $os) { + $release[$releaseNum]['filelist']['ignore'][] = + array( + 'attribs' => array( + 'name' => $file, + ), + ); + } + } + if (!count($release[$releaseNum]['filelist']['install'])) { + unset($release[$releaseNum]['filelist']['install']); + } + if (!count($release[$releaseNum]['filelist']['ignore'])) { + unset($release[$releaseNum]['filelist']['ignore']); + } + } + if (count($generic) || count($genericIgnore)) { + $release[count($oses)] = array(); + if (count($generic)) { + foreach ($generic as $file) { + if (isset($package['install-as'][$file])) { + $installas = $package['install-as'][$file]; + } else { + $installas = $file; + } + $release[count($oses)]['filelist']['install'][] = + array( + 'attribs' => array( + 'name' => $file, + 'as' => $installas, + ) + ); + } + } + if (count($genericIgnore)) { + foreach ($genericIgnore as $file) { + $release[count($oses)]['filelist']['ignore'][] = + array( + 'attribs' => array( + 'name' => $file, + ) + ); + } + } + } + // cleanup + foreach ($release as $i => $rel) { + if (isset($rel['filelist']['install']) && + count($rel['filelist']['install']) == 1) { + $release[$i]['filelist']['install'] = + $release[$i]['filelist']['install'][0]; + } + if (isset($rel['filelist']['ignore']) && + count($rel['filelist']['ignore']) == 1) { + $release[$i]['filelist']['ignore'] = + $release[$i]['filelist']['ignore'][0]; + } + } + if (count($release) == 1) { + $release = $release[0]; + } + } else { + // no platform atts, but some install-as atts + foreach ($package['install-as'] as $file => $value) { + $release['filelist']['install'][] = + array( + 'attribs' => array( + 'name' => $file, + 'as' => $value + ) + ); + } + if (count($release['filelist']['install']) == 1) { + $release['filelist']['install'] = $release['filelist']['install'][0]; + } + } + } + } + + /** + * @param array + * @return array + * @access private + */ + function _processDep($dep) + { + if ($dep['type'] == 'php') { + if ($dep['rel'] == 'has') { + // come on - everyone has php! + return false; + } + } + $php = array(); + if ($dep['type'] != 'php') { + $php['name'] = $dep['name']; + if ($dep['type'] == 'pkg') { + $php['channel'] = 'pear.php.net'; + } + } + switch ($dep['rel']) { + case 'gt' : + $php['min'] = $dep['version']; + $php['exclude'] = $dep['version']; + break; + case 'ge' : + if (!isset($dep['version'])) { + if ($dep['type'] == 'php') { + if (isset($dep['name'])) { + $dep['version'] = $dep['name']; + } + } + } + $php['min'] = $dep['version']; + break; + case 'lt' : + $php['max'] = $dep['version']; + $php['exclude'] = $dep['version']; + break; + case 'le' : + $php['max'] = $dep['version']; + break; + case 'eq' : + $php['min'] = $dep['version']; + $php['max'] = $dep['version']; + break; + case 'ne' : + $php['exclude'] = $dep['version']; + break; + case 'not' : + $php['conflicts'] = 'yes'; + break; + } + return $php; + } + + /** + * @param array + * @return array + */ + function _processPhpDeps($deps) + { + $test = array(); + foreach ($deps as $dep) { + $test[] = $this->_processDep($dep); + } + $min = array(); + $max = array(); + foreach ($test as $dep) { + if (!$dep) { + continue; + } + if (isset($dep['min'])) { + $min[$dep['min']] = count($min); + } + if (isset($dep['max'])) { + $max[$dep['max']] = count($max); + } + } + if (count($min) > 0) { + uksort($min, 'version_compare'); + } + if (count($max) > 0) { + uksort($max, 'version_compare'); + } + if (count($min)) { + // get the highest minimum + $min = array_pop($a = array_flip($min)); + } else { + $min = false; + } + if (count($max)) { + // get the lowest maximum + $max = array_shift($a = array_flip($max)); + } else { + $max = false; + } + if ($min) { + $php['min'] = $min; + } + if ($max) { + $php['max'] = $max; + } + $exclude = array(); + foreach ($test as $dep) { + if (!isset($dep['exclude'])) { + continue; + } + $exclude[] = $dep['exclude']; + } + if (count($exclude)) { + $php['exclude'] = $exclude; + } + return $php; + } + + /** + * process multiple dependencies that have a name, like package deps + * @param array + * @return array + * @access private + */ + function _processMultipleDepsName($deps) + { + $tests = array(); + foreach ($deps as $name => $dep) { + foreach ($dep as $d) { + $tests[$name][] = $this->_processDep($d); + } + } + foreach ($tests as $name => $test) { + $php = array(); + $min = array(); + $max = array(); + $php['name'] = $name; + foreach ($test as $dep) { + if (!$dep) { + continue; + } + if (isset($dep['channel'])) { + $php['channel'] = 'pear.php.net'; + } + if (isset($dep['conflicts']) && $dep['conflicts'] == 'yes') { + $php['conflicts'] = 'yes'; + } + if (isset($dep['min'])) { + $min[$dep['min']] = count($min); + } + if (isset($dep['max'])) { + $max[$dep['max']] = count($max); + } + } + if (count($min) > 0) { + uksort($min, 'version_compare'); + } + if (count($max) > 0) { + uksort($max, 'version_compare'); + } + if (count($min)) { + // get the highest minimum + $min = array_pop($a = array_flip($min)); + } else { + $min = false; + } + if (count($max)) { + // get the lowest maximum + $max = array_shift($a = array_flip($max)); + } else { + $max = false; + } + if ($min) { + $php['min'] = $min; + } + if ($max) { + $php['max'] = $max; + } + $exclude = array(); + foreach ($test as $dep) { + if (!isset($dep['exclude'])) { + continue; + } + $exclude[] = $dep['exclude']; + } + if (count($exclude)) { + $php['exclude'] = $exclude; + } + $ret[] = $php; + } + return $ret; + } +} +?> \ No newline at end of file diff --git a/PEAR/PackageFile/Generator/v2.php b/PEAR/PackageFile/Generator/v2.php new file mode 100644 index 0000000..fb5f58e --- /dev/null +++ b/PEAR/PackageFile/Generator/v2.php @@ -0,0 +1,1529 @@ + + * @author Stephan Schmidt (original XML_Serializer code) + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v2.php,v 1.37 2007/06/10 04:16:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * file/dir manipulation routines + */ +require_once 'System.php'; +/** + * This class converts a PEAR_PackageFile_v2 object into any output format. + * + * Supported output formats include array, XML string (using S. Schmidt's + * XML_Serializer, slightly customized) + * @category pear + * @package PEAR + * @author Greg Beaver + * @author Stephan Schmidt (original XML_Serializer code) + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_Generator_v2 +{ + /** + * default options for the serialization + * @access private + * @var array $_defaultOptions + */ + var $_defaultOptions = array( + 'indent' => ' ', // string used for indentation + 'linebreak' => "\n", // string used for newlines + 'typeHints' => false, // automatically add type hin attributes + 'addDecl' => true, // add an XML declaration + 'defaultTagName' => 'XML_Serializer_Tag', // tag used for indexed arrays or invalid names + 'classAsTagName' => false, // use classname for objects in indexed arrays + 'keyAttribute' => '_originalKey', // attribute where original key is stored + 'typeAttribute' => '_type', // attribute for type (only if typeHints => true) + 'classAttribute' => '_class', // attribute for class of objects (only if typeHints => true) + 'scalarAsAttributes' => false, // scalar values (strings, ints,..) will be serialized as attribute + 'prependAttributes' => '', // prepend string for attributes + 'indentAttributes' => false, // indent the attributes, if set to '_auto', it will indent attributes so they all start at the same column + 'mode' => 'simplexml', // use 'simplexml' to use parent name as tagname if transforming an indexed array + 'addDoctype' => false, // add a doctype declaration + 'doctype' => null, // supply a string or an array with id and uri ({@see PEAR_PackageFile_Generator_v2_PEAR_PackageFile_Generator_v2_XML_Util::getDoctypeDeclaration()} + 'rootName' => 'package', // name of the root tag + 'rootAttributes' => array( + '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', + ), // attributes of the root tag + 'attributesArray' => 'attribs', // all values in this key will be treated as attributes + 'contentName' => '_content', // this value will be used directly as content, instead of creating a new tag, may only be used in conjuction with attributesArray + 'beautifyFilelist' => false, + 'encoding' => 'UTF-8', + ); + + /** + * options for the serialization + * @access private + * @var array $options + */ + var $options = array(); + + /** + * current tag depth + * @var integer $_tagDepth + */ + var $_tagDepth = 0; + + /** + * serilialized representation of the data + * @var string $_serializedData + */ + var $_serializedData = null; + /** + * @var PEAR_PackageFile_v2 + */ + var $_packagefile; + /** + * @param PEAR_PackageFile_v2 + */ + function PEAR_PackageFile_Generator_v2(&$packagefile) + { + $this->_packagefile = &$packagefile; + } + + /** + * @return string + */ + function getPackagerVersion() + { + return '1.6.1'; + } + + /** + * @param PEAR_Packager + * @param bool generate a .tgz or a .tar + * @param string|null temporary directory to package in + */ + function toTgz(&$packager, $compress = true, $where = null) + { + $a = null; + return $this->toTgz2($packager, $a, $compress, $where); + } + + /** + * Package up both a package.xml and package2.xml for the same release + * @param PEAR_Packager + * @param PEAR_PackageFile_v1 + * @param bool generate a .tgz or a .tar + * @param string|null temporary directory to package in + */ + function toTgz2(&$packager, &$pf1, $compress = true, $where = null) + { + require_once 'Archive/Tar.php'; + if (!$this->_packagefile->isEquivalent($pf1)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: "' . + basename($pf1->getPackageFile()) . + '" is not equivalent to "' . basename($this->_packagefile->getPackageFile()) + . '"'); + } + if ($where === null) { + if (!($where = System::mktemp(array('-d')))) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: mktemp failed'); + } + } elseif (!@System::mkDir(array('-p', $where))) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: "' . $where . '" could' . + ' not be created'); + } + if (file_exists($where . DIRECTORY_SEPARATOR . 'package.xml') && + !is_file($where . DIRECTORY_SEPARATOR . 'package.xml')) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: unable to save package.xml as' . + ' "' . $where . DIRECTORY_SEPARATOR . 'package.xml"'); + } + if (!$this->_packagefile->validate(PEAR_VALIDATE_PACKAGING)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: invalid package.xml'); + } + $ext = $compress ? '.tgz' : '.tar'; + $pkgver = $this->_packagefile->getPackage() . '-' . $this->_packagefile->getVersion(); + $dest_package = getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext; + if (file_exists(getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext) && + !is_file(getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: cannot create tgz file "' . + getcwd() . DIRECTORY_SEPARATOR . $pkgver . $ext . '"'); + } + if ($pkgfile = $this->_packagefile->getPackageFile()) { + $pkgdir = dirname(realpath($pkgfile)); + $pkgfile = basename($pkgfile); + } else { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: package file object must ' . + 'be created from a real file'); + } + // {{{ Create the package file list + $filelist = array(); + $i = 0; + $this->_packagefile->flattenFilelist(); + $contents = $this->_packagefile->getContents(); + if (isset($contents['bundledpackage'])) { // bundles of packages + $contents = $contents['bundledpackage']; + if (!isset($contents[0])) { + $contents = array($contents); + } + $packageDir = $where; + foreach ($contents as $i => $package) { + $fname = $package; + $file = $pkgdir . DIRECTORY_SEPARATOR . $fname; + if (!file_exists($file)) { + return $packager->raiseError("File does not exist: $fname"); + } + $tfile = $packageDir . DIRECTORY_SEPARATOR . $fname; + System::mkdir(array('-p', dirname($tfile))); + copy($file, $tfile); + $filelist[$i++] = $tfile; + $packager->log(2, "Adding package $fname"); + } + } else { // normal packages + $contents = $contents['dir']['file']; + if (!isset($contents[0])) { + $contents = array($contents); + } + + $packageDir = $where; + foreach ($contents as $i => $file) { + $fname = $file['attribs']['name']; + $atts = $file['attribs']; + $orig = $file; + $file = $pkgdir . DIRECTORY_SEPARATOR . $fname; + if (!file_exists($file)) { + return $packager->raiseError("File does not exist: $fname"); + } else { + $tfile = $packageDir . DIRECTORY_SEPARATOR . $fname; + unset($orig['attribs']); + if (count($orig)) { // file with tasks + // run any package-time tasks + $contents = file_get_contents($file); + foreach ($orig as $tag => $raw) { + $tag = str_replace( + array($this->_packagefile->getTasksNs() . ':', '-'), + array('', '_'), $tag); + $task = "PEAR_Task_$tag"; + $task = &new $task($this->_packagefile->_config, + $this->_packagefile->_logger, + PEAR_TASK_PACKAGE); + $task->init($raw, $atts, null); + $res = $task->startSession($this->_packagefile, $contents, $tfile); + if (!$res) { + continue; // skip this task + } + if (PEAR::isError($res)) { + return $res; + } + $contents = $res; // save changes + System::mkdir(array('-p', dirname($tfile))); + $wp = fopen($tfile, "wb"); + fwrite($wp, $contents); + fclose($wp); + } + } + if (!file_exists($tfile)) { + System::mkdir(array('-p', dirname($tfile))); + copy($file, $tfile); + } + $filelist[$i++] = $tfile; + $this->_packagefile->setFileAttribute($fname, 'md5sum', md5_file($tfile), $i - 1); + $packager->log(2, "Adding file $fname"); + } + } + } + // }}} + if ($pf1 !== null) { + $name = 'package2.xml'; + } else { + $name = 'package.xml'; + } + $packagexml = $this->toPackageFile($where, PEAR_VALIDATE_PACKAGING, $name); + if ($packagexml) { + $tar =& new Archive_Tar($dest_package, $compress); + $tar->setErrorHandling(PEAR_ERROR_RETURN); // XXX Don't print errors + // ----- Creates with the package.xml file + $ok = $tar->createModify(array($packagexml), '', $where); + if (PEAR::isError($ok)) { + return $packager->raiseError($ok); + } elseif (!$ok) { + return $packager->raiseError('PEAR_Packagefile_v2::toTgz(): adding ' . $name . + ' failed'); + } + // ----- Add the content of the package + if (!$tar->addModify($filelist, $pkgver, $where)) { + return $packager->raiseError( + 'PEAR_Packagefile_v2::toTgz(): tarball creation failed'); + } + // add the package.xml version 1.0 + if ($pf1 !== null) { + $pfgen = &$pf1->getDefaultGenerator(); + $packagexml1 = $pfgen->toPackageFile($where, PEAR_VALIDATE_PACKAGING, + 'package.xml', true); + if (!$tar->addModify(array($packagexml1), '', $where)) { + return $packager->raiseError( + 'PEAR_Packagefile_v2::toTgz(): adding package.xml failed'); + } + } + return $dest_package; + } + } + + function toPackageFile($where = null, $state = PEAR_VALIDATE_NORMAL, $name = 'package.xml') + { + if (!$this->_packagefile->validate($state)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toPackageFile: invalid package.xml', + null, null, null, $this->_packagefile->getValidationWarnings()); + } + if ($where === null) { + if (!($where = System::mktemp(array('-d')))) { + return PEAR::raiseError('PEAR_Packagefile_v2::toPackageFile: mktemp failed'); + } + } elseif (!@System::mkDir(array('-p', $where))) { + return PEAR::raiseError('PEAR_Packagefile_v2::toPackageFile: "' . $where . '" could' . + ' not be created'); + } + $newpkgfile = $where . DIRECTORY_SEPARATOR . $name; + $np = @fopen($newpkgfile, 'wb'); + if (!$np) { + return PEAR::raiseError('PEAR_Packagefile_v2::toPackageFile: unable to save ' . + "$name as $newpkgfile"); + } + fwrite($np, $this->toXml($state)); + fclose($np); + return $newpkgfile; + } + + function &toV2() + { + return $this->_packagefile; + } + + /** + * Return an XML document based on the package info (as returned + * by the PEAR_Common::infoFrom* methods). + * + * @return string XML data + */ + function toXml($state = PEAR_VALIDATE_NORMAL, $options = array()) + { + $this->_packagefile->setDate(date('Y-m-d')); + $this->_packagefile->setTime(date('H:i:s')); + if (!$this->_packagefile->validate($state)) { + return false; + } + if (is_array($options)) { + $this->options = array_merge($this->_defaultOptions, $options); + } else { + $this->options = $this->_defaultOptions; + } + $arr = $this->_packagefile->getArray(); + if (isset($arr['filelist'])) { + unset($arr['filelist']); + } + if (isset($arr['_lastversion'])) { + unset($arr['_lastversion']); + } + if ($state ^ PEAR_VALIDATE_PACKAGING && !isset($arr['bundle'])) { + $use = $this->_recursiveXmlFilelist($arr['contents']['dir']['file']); + unset($arr['contents']['dir']['file']); + if (isset($use['dir'])) { + $arr['contents']['dir']['dir'] = $use['dir']; + } + if (isset($use['file'])) { + $arr['contents']['dir']['file'] = $use['file']; + } + $this->options['beautifyFilelist'] = true; + } + $arr['attribs']['packagerversion'] = '1.6.1'; + if ($this->serialize($arr, $options)) { + return $this->_serializedData . "\n"; + } + return false; + } + + + function _recursiveXmlFilelist($list) + { + $dirs = array(); + if (isset($list['attribs'])) { + $file = $list['attribs']['name']; + unset($list['attribs']['name']); + $attributes = $list['attribs']; + $this->_addDir($dirs, explode('/', dirname($file)), $file, $attributes); + } else { + foreach ($list as $a) { + $file = $a['attribs']['name']; + $attributes = $a['attribs']; + unset($a['attribs']); + $this->_addDir($dirs, explode('/', dirname($file)), $file, $attributes, $a); + } + } + $this->_formatDir($dirs); + $this->_deFormat($dirs); + return $dirs; + } + + function _addDir(&$dirs, $dir, $file = null, $attributes = null, $tasks = null) + { + if (!$tasks) { + $tasks = array(); + } + if ($dir == array() || $dir == array('.')) { + $dirs['file'][basename($file)] = $tasks; + $attributes['name'] = basename($file); + $dirs['file'][basename($file)]['attribs'] = $attributes; + return; + } + $curdir = array_shift($dir); + if (!isset($dirs['dir'][$curdir])) { + $dirs['dir'][$curdir] = array(); + } + $this->_addDir($dirs['dir'][$curdir], $dir, $file, $attributes, $tasks); + } + + function _formatDir(&$dirs) + { + if (!count($dirs)) { + return array(); + } + $newdirs = array(); + if (isset($dirs['dir'])) { + $newdirs['dir'] = $dirs['dir']; + } + if (isset($dirs['file'])) { + $newdirs['file'] = $dirs['file']; + } + $dirs = $newdirs; + if (isset($dirs['dir'])) { + uksort($dirs['dir'], 'strnatcasecmp'); + foreach ($dirs['dir'] as $dir => $contents) { + $this->_formatDir($dirs['dir'][$dir]); + } + } + if (isset($dirs['file'])) { + uksort($dirs['file'], 'strnatcasecmp'); + }; + } + + function _deFormat(&$dirs) + { + if (!count($dirs)) { + return array(); + } + $newdirs = array(); + if (isset($dirs['dir'])) { + foreach ($dirs['dir'] as $dir => $contents) { + $newdir = array(); + $newdir['attribs']['name'] = $dir; + $this->_deFormat($contents); + foreach ($contents as $tag => $val) { + $newdir[$tag] = $val; + } + $newdirs['dir'][] = $newdir; + } + if (count($newdirs['dir']) == 1) { + $newdirs['dir'] = $newdirs['dir'][0]; + } + } + if (isset($dirs['file'])) { + foreach ($dirs['file'] as $name => $file) { + $newdirs['file'][] = $file; + } + if (count($newdirs['file']) == 1) { + $newdirs['file'] = $newdirs['file'][0]; + } + } + $dirs = $newdirs; + } + + /** + * reset all options to default options + * + * @access public + * @see setOption(), XML_Unserializer() + */ + function resetOptions() + { + $this->options = $this->_defaultOptions; + } + + /** + * set an option + * + * You can use this method if you do not want to set all options in the constructor + * + * @access public + * @see resetOption(), XML_Serializer() + */ + function setOption($name, $value) + { + $this->options[$name] = $value; + } + + /** + * sets several options at once + * + * You can use this method if you do not want to set all options in the constructor + * + * @access public + * @see resetOption(), XML_Unserializer(), setOption() + */ + function setOptions($options) + { + $this->options = array_merge($this->options, $options); + } + + /** + * serialize data + * + * @access public + * @param mixed $data data to serialize + * @return boolean true on success, pear error on failure + */ + function serialize($data, $options = null) + { + // if options have been specified, use them instead + // of the previously defined ones + if (is_array($options)) { + $optionsBak = $this->options; + if (isset($options['overrideOptions']) && $options['overrideOptions'] == true) { + $this->options = array_merge($this->_defaultOptions, $options); + } else { + $this->options = array_merge($this->options, $options); + } + } + else { + $optionsBak = null; + } + + // start depth is zero + $this->_tagDepth = 0; + + $this->_serializedData = ''; + // serialize an array + if (is_array($data)) { + if (isset($this->options['rootName'])) { + $tagName = $this->options['rootName']; + } else { + $tagName = 'array'; + } + + $this->_serializedData .= $this->_serializeArray($data, $tagName, $this->options['rootAttributes']); + } + + // add doctype declaration + if ($this->options['addDoctype'] === true) { + $this->_serializedData = PEAR_PackageFile_Generator_v2_XML_Util::getDoctypeDeclaration($tagName, $this->options['doctype']) + . $this->options['linebreak'] + . $this->_serializedData; + } + + // build xml declaration + if ($this->options['addDecl']) { + $atts = array(); + if (isset($this->options['encoding']) ) { + $encoding = $this->options['encoding']; + } else { + $encoding = null; + } + $this->_serializedData = PEAR_PackageFile_Generator_v2_XML_Util::getXMLDeclaration('1.0', $encoding) + . $this->options['linebreak'] + . $this->_serializedData; + } + + + if ($optionsBak !== null) { + $this->options = $optionsBak; + } + + return true; + } + + /** + * get the result of the serialization + * + * @access public + * @return string serialized XML + */ + function getSerializedData() + { + if ($this->_serializedData == null ) { + return $this->raiseError('No serialized data available. Use XML_Serializer::serialize() first.', XML_SERIALIZER_ERROR_NO_SERIALIZATION); + } + return $this->_serializedData; + } + + /** + * serialize any value + * + * This method checks for the type of the value and calls the appropriate method + * + * @access private + * @param mixed $value + * @param string $tagName + * @param array $attributes + * @return string + */ + function _serializeValue($value, $tagName = null, $attributes = array()) + { + if (is_array($value)) { + $xml = $this->_serializeArray($value, $tagName, $attributes); + } elseif (is_object($value)) { + $xml = $this->_serializeObject($value, $tagName); + } else { + $tag = array( + 'qname' => $tagName, + 'attributes' => $attributes, + 'content' => $value + ); + $xml = $this->_createXMLTag($tag); + } + return $xml; + } + + /** + * serialize an array + * + * @access private + * @param array $array array to serialize + * @param string $tagName name of the root tag + * @param array $attributes attributes for the root tag + * @return string $string serialized data + * @uses PEAR_PackageFile_Generator_v2_XML_Util::isValidName() to check, whether key has to be substituted + */ + function _serializeArray(&$array, $tagName = null, $attributes = array()) + { + $_content = null; + + /** + * check for special attributes + */ + if ($this->options['attributesArray'] !== null) { + if (isset($array[$this->options['attributesArray']])) { + $attributes = $array[$this->options['attributesArray']]; + unset($array[$this->options['attributesArray']]); + } + /** + * check for special content + */ + if ($this->options['contentName'] !== null) { + if (isset($array[$this->options['contentName']])) { + $_content = $array[$this->options['contentName']]; + unset($array[$this->options['contentName']]); + } + } + } + + /* + * if mode is set to simpleXML, check whether + * the array is associative or indexed + */ + if (is_array($array) && $this->options['mode'] == 'simplexml') { + $indexed = true; + if (!count($array)) { + $indexed = false; + } + foreach ($array as $key => $val) { + if (!is_int($key)) { + $indexed = false; + break; + } + } + + if ($indexed && $this->options['mode'] == 'simplexml') { + $string = ''; + foreach ($array as $key => $val) { + if ($this->options['beautifyFilelist'] && $tagName == 'dir') { + if (!isset($this->_curdir)) { + $this->_curdir = ''; + } + $savedir = $this->_curdir; + if (isset($val['attribs'])) { + if ($val['attribs']['name'] == '/') { + $this->_curdir = '/'; + } else { + if ($this->_curdir == '/') { + $this->_curdir = ''; + } + $this->_curdir .= '/' . $val['attribs']['name']; + } + } + } + $string .= $this->_serializeValue( $val, $tagName, $attributes); + if ($this->options['beautifyFilelist'] && $tagName == 'dir') { + $string .= ' '; + if (empty($savedir)) { + unset($this->_curdir); + } else { + $this->_curdir = $savedir; + } + } + + $string .= $this->options['linebreak']; + // do indentation + if ($this->options['indent']!==null && $this->_tagDepth>0) { + $string .= str_repeat($this->options['indent'], $this->_tagDepth); + } + } + return rtrim($string); + } + } + + if ($this->options['scalarAsAttributes'] === true) { + foreach ($array as $key => $value) { + if (is_scalar($value) && (PEAR_PackageFile_Generator_v2_XML_Util::isValidName($key) === true)) { + unset($array[$key]); + $attributes[$this->options['prependAttributes'].$key] = $value; + } + } + } + + // check for empty array => create empty tag + if (empty($array)) { + $tag = array( + 'qname' => $tagName, + 'content' => $_content, + 'attributes' => $attributes + ); + + } else { + $this->_tagDepth++; + $tmp = $this->options['linebreak']; + foreach ($array as $key => $value) { + // do indentation + if ($this->options['indent']!==null && $this->_tagDepth>0) { + $tmp .= str_repeat($this->options['indent'], $this->_tagDepth); + } + + // copy key + $origKey = $key; + // key cannot be used as tagname => use default tag + $valid = PEAR_PackageFile_Generator_v2_XML_Util::isValidName($key); + if (PEAR::isError($valid)) { + if ($this->options['classAsTagName'] && is_object($value)) { + $key = get_class($value); + } else { + $key = $this->options['defaultTagName']; + } + } + $atts = array(); + if ($this->options['typeHints'] === true) { + $atts[$this->options['typeAttribute']] = gettype($value); + if ($key !== $origKey) { + $atts[$this->options['keyAttribute']] = (string)$origKey; + } + + } + if ($this->options['beautifyFilelist'] && $key == 'dir') { + if (!isset($this->_curdir)) { + $this->_curdir = ''; + } + $savedir = $this->_curdir; + if (isset($value['attribs'])) { + if ($value['attribs']['name'] == '/') { + $this->_curdir = '/'; + } else { + $this->_curdir .= '/' . $value['attribs']['name']; + } + } + } + + if (is_string($value) && $value && ($value{strlen($value) - 1} == "\n")) { + $value .= str_repeat($this->options['indent'], $this->_tagDepth); + } + $tmp .= $this->_createXMLTag(array( + 'qname' => $key, + 'attributes' => $atts, + 'content' => $value ) + ); + if ($this->options['beautifyFilelist'] && $key == 'dir') { + if (isset($value['attribs'])) { + $tmp .= ' '; + if (empty($savedir)) { + unset($this->_curdir); + } else { + $this->_curdir = $savedir; + } + } + } + $tmp .= $this->options['linebreak']; + } + + $this->_tagDepth--; + if ($this->options['indent']!==null && $this->_tagDepth>0) { + $tmp .= str_repeat($this->options['indent'], $this->_tagDepth); + } + + if (trim($tmp) === '') { + $tmp = null; + } + + $tag = array( + 'qname' => $tagName, + 'content' => $tmp, + 'attributes' => $attributes + ); + } + if ($this->options['typeHints'] === true) { + if (!isset($tag['attributes'][$this->options['typeAttribute']])) { + $tag['attributes'][$this->options['typeAttribute']] = 'array'; + } + } + + $string = $this->_createXMLTag($tag, false); + return $string; + } + + /** + * create a tag from an array + * this method awaits an array in the following format + * array( + * 'qname' => $tagName, + * 'attributes' => array(), + * 'content' => $content, // optional + * 'namespace' => $namespace // optional + * 'namespaceUri' => $namespaceUri // optional + * ) + * + * @access private + * @param array $tag tag definition + * @param boolean $replaceEntities whether to replace XML entities in content or not + * @return string $string XML tag + */ + function _createXMLTag( $tag, $replaceEntities = true ) + { + if ($this->options['indentAttributes'] !== false) { + $multiline = true; + $indent = str_repeat($this->options['indent'], $this->_tagDepth); + + if ($this->options['indentAttributes'] == '_auto') { + $indent .= str_repeat(' ', (strlen($tag['qname'])+2)); + + } else { + $indent .= $this->options['indentAttributes']; + } + } else { + $multiline = false; + $indent = false; + } + + if (is_array($tag['content'])) { + if (empty($tag['content'])) { + $tag['content'] = ''; + } + } elseif(is_scalar($tag['content']) && (string)$tag['content'] == '') { + $tag['content'] = ''; + } + + if (is_scalar($tag['content']) || is_null($tag['content'])) { + if ($this->options['encoding'] == 'UTF-8' && + version_compare(phpversion(), '5.0.0', 'lt')) { + $encoding = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_UTF8_XML; + } else { + $encoding = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML; + } + $tag = PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray($tag, $replaceEntities, $multiline, $indent, $this->options['linebreak'], $encoding); + } elseif (is_array($tag['content'])) { + $tag = $this->_serializeArray($tag['content'], $tag['qname'], $tag['attributes']); + } elseif (is_object($tag['content'])) { + $tag = $this->_serializeObject($tag['content'], $tag['qname'], $tag['attributes']); + } elseif (is_resource($tag['content'])) { + settype($tag['content'], 'string'); + $tag = PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray($tag, $replaceEntities); + } + return $tag; + } +} + +// well, it's one way to do things without extra deps ... +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2002 The PHP Group | +// +----------------------------------------------------------------------+ +// | 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. | +// +----------------------------------------------------------------------+ +// | Authors: Stephan Schmidt | +// +----------------------------------------------------------------------+ +// +// $Id: v2.php,v 1.37 2007/06/10 04:16:51 cellog Exp $ + +/** + * error code for invalid chars in XML name + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ERROR_INVALID_CHARS", 51); + +/** + * error code for invalid chars in XML name + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ERROR_INVALID_START", 52); + +/** + * error code for non-scalar tag content + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ERROR_NON_SCALAR_CONTENT", 60); + +/** + * error code for missing tag name + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ERROR_NO_TAG_NAME", 61); + +/** + * replace XML entities + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_REPLACE_ENTITIES", 1); + +/** + * embedd content in a CData Section + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_CDATA_SECTION", 2); + +/** + * do not replace entitites + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_NONE", 0); + +/** + * replace all XML entitites + * This setting will replace <, >, ", ' and & + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML", 1); + +/** + * replace only required XML entitites + * This setting will replace <, " and & + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML_REQUIRED", 2); + +/** + * replace HTML entitites + * @link http://www.php.net/htmlentities + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_HTML", 3); + +/** + * replace all XML entitites, and encode from ISO-8859-1 to UTF-8 + * This setting will replace <, >, ", ' and & + */ +define("PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_UTF8_XML", 4); + +/** + * utility class for working with XML documents + * + * customized version of XML_Util 0.6.0 + * + * @category XML + * @package PEAR + * @version 0.6.0 + * @author Stephan Schmidt + * @author Gregory Beaver + */ +class PEAR_PackageFile_Generator_v2_XML_Util { + + /** + * return API version + * + * @access public + * @static + * @return string $version API version + */ + function apiVersion() + { + return "0.6"; + } + + /** + * replace XML entities + * + * With the optional second parameter, you may select, which + * entities should be replaced. + * + * + * require_once 'XML/Util.php'; + * + * // replace XML entites: + * $string = PEAR_PackageFile_Generator_v2_XML_Util::replaceEntities("This string contains < & >."); + * + * + * @access public + * @static + * @param string string where XML special chars should be replaced + * @param integer setting for entities in attribute values (one of PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML, PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML_REQUIRED, PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_HTML) + * @return string string with replaced chars + */ + function replaceEntities($string, $replaceEntities = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML) + { + switch ($replaceEntities) { + case PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_UTF8_XML: + return strtr(utf8_encode($string),array( + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + '\'' => ''' )); + break; + case PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML: + return strtr($string,array( + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + '\'' => ''' )); + break; + case PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML_REQUIRED: + return strtr($string,array( + '&' => '&', + '<' => '<', + '"' => '"' )); + break; + case PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_HTML: + return htmlspecialchars($string); + break; + } + return $string; + } + + /** + * build an xml declaration + * + * + * require_once 'XML/Util.php'; + * + * // get an XML declaration: + * $xmlDecl = PEAR_PackageFile_Generator_v2_XML_Util::getXMLDeclaration("1.0", "UTF-8", true); + * + * + * @access public + * @static + * @param string $version xml version + * @param string $encoding character encoding + * @param boolean $standAlone document is standalone (or not) + * @return string $decl xml declaration + * @uses PEAR_PackageFile_Generator_v2_XML_Util::attributesToString() to serialize the attributes of the XML declaration + */ + function getXMLDeclaration($version = "1.0", $encoding = null, $standalone = null) + { + $attributes = array( + "version" => $version, + ); + // add encoding + if ($encoding !== null) { + $attributes["encoding"] = $encoding; + } + // add standalone, if specified + if ($standalone !== null) { + $attributes["standalone"] = $standalone ? "yes" : "no"; + } + + return sprintf("", PEAR_PackageFile_Generator_v2_XML_Util::attributesToString($attributes, false)); + } + + /** + * build a document type declaration + * + * + * require_once 'XML/Util.php'; + * + * // get a doctype declaration: + * $xmlDecl = PEAR_PackageFile_Generator_v2_XML_Util::getDocTypeDeclaration("rootTag","myDocType.dtd"); + * + * + * @access public + * @static + * @param string $root name of the root tag + * @param string $uri uri of the doctype definition (or array with uri and public id) + * @param string $internalDtd internal dtd entries + * @return string $decl doctype declaration + * @since 0.2 + */ + function getDocTypeDeclaration($root, $uri = null, $internalDtd = null) + { + if (is_array($uri)) { + $ref = sprintf( ' PUBLIC "%s" "%s"', $uri["id"], $uri["uri"] ); + } elseif (!empty($uri)) { + $ref = sprintf( ' SYSTEM "%s"', $uri ); + } else { + $ref = ""; + } + + if (empty($internalDtd)) { + return sprintf("", $root, $ref); + } else { + return sprintf("", $root, $ref, $internalDtd); + } + } + + /** + * create string representation of an attribute list + * + * + * require_once 'XML/Util.php'; + * + * // build an attribute string + * $att = array( + * "foo" => "bar", + * "argh" => "tomato" + * ); + * + * $attList = PEAR_PackageFile_Generator_v2_XML_Util::attributesToString($att); + * + * + * @access public + * @static + * @param array $attributes attribute array + * @param boolean|array $sort sort attribute list alphabetically, may also be an assoc array containing the keys 'sort', 'multiline', 'indent', 'linebreak' and 'entities' + * @param boolean $multiline use linebreaks, if more than one attribute is given + * @param string $indent string used for indentation of multiline attributes + * @param string $linebreak string used for linebreaks of multiline attributes + * @param integer $entities setting for entities in attribute values (one of PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_NONE, PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML, PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML_REQUIRED, PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_HTML) + * @return string string representation of the attributes + * @uses PEAR_PackageFile_Generator_v2_XML_Util::replaceEntities() to replace XML entities in attribute values + * @todo allow sort also to be an options array + */ + function attributesToString($attributes, $sort = true, $multiline = false, $indent = ' ', $linebreak = "\n", $entities = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML) + { + /** + * second parameter may be an array + */ + if (is_array($sort)) { + if (isset($sort['multiline'])) { + $multiline = $sort['multiline']; + } + if (isset($sort['indent'])) { + $indent = $sort['indent']; + } + if (isset($sort['linebreak'])) { + $multiline = $sort['linebreak']; + } + if (isset($sort['entities'])) { + $entities = $sort['entities']; + } + if (isset($sort['sort'])) { + $sort = $sort['sort']; + } else { + $sort = true; + } + } + $string = ''; + if (is_array($attributes) && !empty($attributes)) { + if ($sort) { + ksort($attributes); + } + if( !$multiline || count($attributes) == 1) { + foreach ($attributes as $key => $value) { + if ($entities != PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_NONE) { + $value = PEAR_PackageFile_Generator_v2_XML_Util::replaceEntities($value, $entities); + } + $string .= ' '.$key.'="'.$value.'"'; + } + } else { + $first = true; + foreach ($attributes as $key => $value) { + if ($entities != PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_NONE) { + $value = PEAR_PackageFile_Generator_v2_XML_Util::replaceEntities($value, $entities); + } + if ($first) { + $string .= " ".$key.'="'.$value.'"'; + $first = false; + } else { + $string .= $linebreak.$indent.$key.'="'.$value.'"'; + } + } + } + } + return $string; + } + + /** + * create a tag + * + * This method will call PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray(), which + * is more flexible. + * + * + * require_once 'XML/Util.php'; + * + * // create an XML tag: + * $tag = PEAR_PackageFile_Generator_v2_XML_Util::createTag("myNs:myTag", array("foo" => "bar"), "This is inside the tag", "http://www.w3c.org/myNs#"); + * + * + * @access public + * @static + * @param string $qname qualified tagname (including namespace) + * @param array $attributes array containg attributes + * @param mixed $content + * @param string $namespaceUri URI of the namespace + * @param integer $replaceEntities whether to replace XML special chars in content, embedd it in a CData section or none of both + * @param boolean $multiline whether to create a multiline tag where each attribute gets written to a single line + * @param string $indent string used to indent attributes (_auto indents attributes so they start at the same column) + * @param string $linebreak string used for linebreaks + * @param string $encoding encoding that should be used to translate content + * @return string $string XML tag + * @see PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray() + * @uses PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray() to create the tag + */ + function createTag($qname, $attributes = array(), $content = null, $namespaceUri = null, $replaceEntities = PEAR_PackageFile_Generator_v2_XML_Util_REPLACE_ENTITIES, $multiline = false, $indent = "_auto", $linebreak = "\n", $encoding = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML) + { + $tag = array( + "qname" => $qname, + "attributes" => $attributes + ); + + // add tag content + if ($content !== null) { + $tag["content"] = $content; + } + + // add namespace Uri + if ($namespaceUri !== null) { + $tag["namespaceUri"] = $namespaceUri; + } + + return PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray($tag, $replaceEntities, $multiline, $indent, $linebreak, $encoding); + } + + /** + * create a tag from an array + * this method awaits an array in the following format + *
+    * array(
+    *  "qname"        => $qname         // qualified name of the tag
+    *  "namespace"    => $namespace     // namespace prefix (optional, if qname is specified or no namespace)
+    *  "localpart"    => $localpart,    // local part of the tagname (optional, if qname is specified)
+    *  "attributes"   => array(),       // array containing all attributes (optional)
+    *  "content"      => $content,      // tag content (optional)
+    *  "namespaceUri" => $namespaceUri  // namespaceUri for the given namespace (optional)
+    *   )
+    * 
+ * + * + * require_once 'XML/Util.php'; + * + * $tag = array( + * "qname" => "foo:bar", + * "namespaceUri" => "http://foo.com", + * "attributes" => array( "key" => "value", "argh" => "fruit&vegetable" ), + * "content" => "I'm inside the tag", + * ); + * // creating a tag with qualified name and namespaceUri + * $string = PEAR_PackageFile_Generator_v2_XML_Util::createTagFromArray($tag); + * + * + * @access public + * @static + * @param array $tag tag definition + * @param integer $replaceEntities whether to replace XML special chars in content, embedd it in a CData section or none of both + * @param boolean $multiline whether to create a multiline tag where each attribute gets written to a single line + * @param string $indent string used to indent attributes (_auto indents attributes so they start at the same column) + * @param string $linebreak string used for linebreaks + * @return string $string XML tag + * @see PEAR_PackageFile_Generator_v2_XML_Util::createTag() + * @uses PEAR_PackageFile_Generator_v2_XML_Util::attributesToString() to serialize the attributes of the tag + * @uses PEAR_PackageFile_Generator_v2_XML_Util::splitQualifiedName() to get local part and namespace of a qualified name + */ + function createTagFromArray($tag, $replaceEntities = PEAR_PackageFile_Generator_v2_XML_Util_REPLACE_ENTITIES, $multiline = false, $indent = "_auto", $linebreak = "\n", $encoding = PEAR_PackageFile_Generator_v2_XML_Util_ENTITIES_XML) + { + if (isset($tag["content"]) && !is_scalar($tag["content"])) { + return PEAR_PackageFile_Generator_v2_XML_Util::raiseError( "Supplied non-scalar value as tag content", PEAR_PackageFile_Generator_v2_XML_Util_ERROR_NON_SCALAR_CONTENT ); + } + + if (!isset($tag['qname']) && !isset($tag['localPart'])) { + return PEAR_PackageFile_Generator_v2_XML_Util::raiseError( 'You must either supply a qualified name (qname) or local tag name (localPart).', PEAR_PackageFile_Generator_v2_XML_Util_ERROR_NO_TAG_NAME ); + } + + // if no attributes hav been set, use empty attributes + if (!isset($tag["attributes"]) || !is_array($tag["attributes"])) { + $tag["attributes"] = array(); + } + + // qualified name is not given + if (!isset($tag["qname"])) { + // check for namespace + if (isset($tag["namespace"]) && !empty($tag["namespace"])) { + $tag["qname"] = $tag["namespace"].":".$tag["localPart"]; + } else { + $tag["qname"] = $tag["localPart"]; + } + // namespace URI is set, but no namespace + } elseif (isset($tag["namespaceUri"]) && !isset($tag["namespace"])) { + $parts = PEAR_PackageFile_Generator_v2_XML_Util::splitQualifiedName($tag["qname"]); + $tag["localPart"] = $parts["localPart"]; + if (isset($parts["namespace"])) { + $tag["namespace"] = $parts["namespace"]; + } + } + + if (isset($tag["namespaceUri"]) && !empty($tag["namespaceUri"])) { + // is a namespace given + if (isset($tag["namespace"]) && !empty($tag["namespace"])) { + $tag["attributes"]["xmlns:".$tag["namespace"]] = $tag["namespaceUri"]; + } else { + // define this Uri as the default namespace + $tag["attributes"]["xmlns"] = $tag["namespaceUri"]; + } + } + + // check for multiline attributes + if ($multiline === true) { + if ($indent === "_auto") { + $indent = str_repeat(" ", (strlen($tag["qname"])+2)); + } + } + + // create attribute list + $attList = PEAR_PackageFile_Generator_v2_XML_Util::attributesToString($tag["attributes"], true, $multiline, $indent, $linebreak ); + if (!isset($tag["content"]) || (string)$tag["content"] == '') { + $tag = sprintf("<%s%s />", $tag["qname"], $attList); + } else { + if ($replaceEntities == PEAR_PackageFile_Generator_v2_XML_Util_REPLACE_ENTITIES) { + $tag["content"] = PEAR_PackageFile_Generator_v2_XML_Util::replaceEntities($tag["content"], $encoding); + } elseif ($replaceEntities == PEAR_PackageFile_Generator_v2_XML_Util_CDATA_SECTION) { + $tag["content"] = PEAR_PackageFile_Generator_v2_XML_Util::createCDataSection($tag["content"]); + } + $tag = sprintf("<%s%s>%s", $tag["qname"], $attList, $tag["content"], $tag["qname"] ); + } + return $tag; + } + + /** + * create a start element + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = PEAR_PackageFile_Generator_v2_XML_Util::createStartElement("myNs:myTag", array("foo" => "bar") ,"http://www.w3c.org/myNs#"); + * + * + * @access public + * @static + * @param string $qname qualified tagname (including namespace) + * @param array $attributes array containg attributes + * @param string $namespaceUri URI of the namespace + * @param boolean $multiline whether to create a multiline tag where each attribute gets written to a single line + * @param string $indent string used to indent attributes (_auto indents attributes so they start at the same column) + * @param string $linebreak string used for linebreaks + * @return string $string XML start element + * @see PEAR_PackageFile_Generator_v2_XML_Util::createEndElement(), PEAR_PackageFile_Generator_v2_XML_Util::createTag() + */ + function createStartElement($qname, $attributes = array(), $namespaceUri = null, $multiline = false, $indent = '_auto', $linebreak = "\n") + { + // if no attributes hav been set, use empty attributes + if (!isset($attributes) || !is_array($attributes)) { + $attributes = array(); + } + + if ($namespaceUri != null) { + $parts = PEAR_PackageFile_Generator_v2_XML_Util::splitQualifiedName($qname); + } + + // check for multiline attributes + if ($multiline === true) { + if ($indent === "_auto") { + $indent = str_repeat(" ", (strlen($qname)+2)); + } + } + + if ($namespaceUri != null) { + // is a namespace given + if (isset($parts["namespace"]) && !empty($parts["namespace"])) { + $attributes["xmlns:".$parts["namespace"]] = $namespaceUri; + } else { + // define this Uri as the default namespace + $attributes["xmlns"] = $namespaceUri; + } + } + + // create attribute list + $attList = PEAR_PackageFile_Generator_v2_XML_Util::attributesToString($attributes, true, $multiline, $indent, $linebreak); + $element = sprintf("<%s%s>", $qname, $attList); + return $element; + } + + /** + * create an end element + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = PEAR_PackageFile_Generator_v2_XML_Util::createEndElement("myNs:myTag"); + * + * + * @access public + * @static + * @param string $qname qualified tagname (including namespace) + * @return string $string XML end element + * @see PEAR_PackageFile_Generator_v2_XML_Util::createStartElement(), PEAR_PackageFile_Generator_v2_XML_Util::createTag() + */ + function createEndElement($qname) + { + $element = sprintf("", $qname); + return $element; + } + + /** + * create an XML comment + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = PEAR_PackageFile_Generator_v2_XML_Util::createComment("I am a comment"); + * + * + * @access public + * @static + * @param string $content content of the comment + * @return string $comment XML comment + */ + function createComment($content) + { + $comment = sprintf("", $content); + return $comment; + } + + /** + * create a CData section + * + * + * require_once 'XML/Util.php'; + * + * // create a CData section + * $tag = PEAR_PackageFile_Generator_v2_XML_Util::createCDataSection("I am content."); + * + * + * @access public + * @static + * @param string $data data of the CData section + * @return string $string CData section with content + */ + function createCDataSection($data) + { + return sprintf("", $data); + } + + /** + * split qualified name and return namespace and local part + * + * + * require_once 'XML/Util.php'; + * + * // split qualified tag + * $parts = PEAR_PackageFile_Generator_v2_XML_Util::splitQualifiedName("xslt:stylesheet"); + * + * the returned array will contain two elements: + *
+    * array(
+    *       "namespace" => "xslt",
+    *       "localPart" => "stylesheet"
+    *      );
+    * 
+ * + * @access public + * @static + * @param string $qname qualified tag name + * @param string $defaultNs default namespace (optional) + * @return array $parts array containing namespace and local part + */ + function splitQualifiedName($qname, $defaultNs = null) + { + if (strstr($qname, ':')) { + $tmp = explode(":", $qname); + return array( + "namespace" => $tmp[0], + "localPart" => $tmp[1] + ); + } + return array( + "namespace" => $defaultNs, + "localPart" => $qname + ); + } + + /** + * check, whether string is valid XML name + * + *

XML names are used for tagname, attribute names and various + * other, lesser known entities.

+ *

An XML name may only consist of alphanumeric characters, + * dashes, undescores and periods, and has to start with a letter + * or an underscore. + *

+ * + * + * require_once 'XML/Util.php'; + * + * // verify tag name + * $result = PEAR_PackageFile_Generator_v2_XML_Util::isValidName("invalidTag?"); + * if (PEAR_PackageFile_Generator_v2_XML_Util::isError($result)) { + * print "Invalid XML name: " . $result->getMessage(); + * } + * + * + * @access public + * @static + * @param string $string string that should be checked + * @return mixed $valid true, if string is a valid XML name, PEAR error otherwise + * @todo support for other charsets + */ + function isValidName($string) + { + // check for invalid chars + if (!preg_match("/^[[:alnum:]_\-.]\\z/", $string{0})) { + return PEAR_PackageFile_Generator_v2_XML_Util::raiseError( "XML names may only start with letter or underscore", PEAR_PackageFile_Generator_v2_XML_Util_ERROR_INVALID_START ); + } + + // check for invalid chars + if (!preg_match("/^([a-zA-Z_]([a-zA-Z0-9_\-\.]*)?:)?[a-zA-Z_]([a-zA-Z0-9_\-\.]+)?\\z/", $string)) { + return PEAR_PackageFile_Generator_v2_XML_Util::raiseError( "XML names may only contain alphanumeric chars, period, hyphen, colon and underscores", PEAR_PackageFile_Generator_v2_XML_Util_ERROR_INVALID_CHARS ); + } + // XML name is valid + return true; + } + + /** + * replacement for PEAR_PackageFile_Generator_v2_XML_Util::raiseError + * + * Avoids the necessity to always require + * PEAR.php + * + * @access public + * @param string error message + * @param integer error code + * @return object PEAR_Error + */ + function raiseError($msg, $code) + { + require_once 'PEAR.php'; + return PEAR::raiseError($msg, $code); + } +} +?> \ No newline at end of file diff --git a/PEAR/PackageFile/Parser/v1.php b/PEAR/PackageFile/Parser/v1.php new file mode 100644 index 0000000..ba23787 --- /dev/null +++ b/PEAR/PackageFile/Parser/v1.php @@ -0,0 +1,461 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v1.php,v 1.22 2006/03/27 05:25:48 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * package.xml abstraction class + */ +require_once 'PEAR/PackageFile/v1.php'; +/** + * Parser for package.xml version 1.0 + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @PEAR-VER@ + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_Parser_v1 +{ + var $_registry; + var $_config; + var $_logger; + /** + * BC hack to allow PEAR_Common::infoFromString() to sort of + * work with the version 2.0 format - there's no filelist though + * @param PEAR_PackageFile_v2 + */ + function fromV2($packagefile) + { + $info = $packagefile->getArray(true); + $ret = new PEAR_PackageFile_v1; + $ret->fromArray($info['old']); + } + + function setConfig(&$c) + { + $this->_config = &$c; + $this->_registry = &$c->getRegistry(); + } + + function setLogger(&$l) + { + $this->_logger = &$l; + } + + /** + * @param string contents of package.xml file, version 1.0 + * @return bool success of parsing + */ + function parse($data, $file, $archive = false) + { + if (!extension_loaded('xml')) { + return PEAR::raiseError('Cannot create xml parser for parsing package.xml, no xml extension'); + } + $xp = xml_parser_create(); + if (!$xp) { + return PEAR::raiseError('Cannot create xml parser for parsing package.xml'); + } + xml_set_object($xp, $this); + xml_set_element_handler($xp, '_element_start_1_0', '_element_end_1_0'); + xml_set_character_data_handler($xp, '_pkginfo_cdata_1_0'); + xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, false); + + $this->element_stack = array(); + $this->_packageInfo = array('provides' => array()); + $this->current_element = false; + unset($this->dir_install); + $this->_packageInfo['filelist'] = array(); + $this->filelist =& $this->_packageInfo['filelist']; + $this->dir_names = array(); + $this->in_changelog = false; + $this->d_i = 0; + $this->cdata = ''; + $this->_isValid = true; + + if (!xml_parse($xp, $data, 1)) { + $code = xml_get_error_code($xp); + $line = xml_get_current_line_number($xp); + xml_parser_free($xp); + return PEAR::raiseError(sprintf("XML error: %s at line %d", + $str = xml_error_string($code), $line), 2); + } + + xml_parser_free($xp); + + $pf = new PEAR_PackageFile_v1; + $pf->setConfig($this->_config); + if (isset($this->_logger)) { + $pf->setLogger($this->_logger); + } + $pf->setPackagefile($file, $archive); + $pf->fromArray($this->_packageInfo); + return $pf; + } + // {{{ _unIndent() + + /** + * Unindent given string + * + * @param string $str The string that has to be unindented. + * @return string + * @access private + */ + function _unIndent($str) + { + // remove leading newlines + $str = preg_replace('/^[\r\n]+/', '', $str); + // find whitespace at the beginning of the first line + $indent_len = strspn($str, " \t"); + $indent = substr($str, 0, $indent_len); + $data = ''; + // remove the same amount of whitespace from following lines + foreach (explode("\n", $str) as $line) { + if (substr($line, 0, $indent_len) == $indent) { + $data .= substr($line, $indent_len) . "\n"; + } + } + return $data; + } + + // Support for package DTD v1.0: + // {{{ _element_start_1_0() + + /** + * XML parser callback for ending elements. Used for version 1.0 + * packages. + * + * @param resource $xp XML parser resource + * @param string $name name of ending element + * + * @return void + * + * @access private + */ + function _element_start_1_0($xp, $name, $attribs) + { + array_push($this->element_stack, $name); + $this->current_element = $name; + $spos = sizeof($this->element_stack) - 2; + $this->prev_element = ($spos >= 0) ? $this->element_stack[$spos] : ''; + $this->current_attributes = $attribs; + $this->cdata = ''; + switch ($name) { + case 'dir': + if ($this->in_changelog) { + break; + } + if (array_key_exists('name', $attribs) && $attribs['name'] != '/') { + $attribs['name'] = preg_replace(array('!\\\\+!', '!/+!'), array('/', '/'), + $attribs['name']); + if (strrpos($attribs['name'], '/') == strlen($attribs['name']) - 1) { + $attribs['name'] = substr($attribs['name'], 0, + strlen($attribs['name']) - 1); + } + if (strpos($attribs['name'], '/') === 0) { + $attribs['name'] = substr($attribs['name'], 1); + } + $this->dir_names[] = $attribs['name']; + } + if (isset($attribs['baseinstalldir'])) { + $this->dir_install = $attribs['baseinstalldir']; + } + if (isset($attribs['role'])) { + $this->dir_role = $attribs['role']; + } + break; + case 'file': + if ($this->in_changelog) { + break; + } + if (isset($attribs['name'])) { + $path = ''; + if (count($this->dir_names)) { + foreach ($this->dir_names as $dir) { + $path .= $dir . '/'; + } + } + $path .= preg_replace(array('!\\\\+!', '!/+!'), array('/', '/'), + $attribs['name']); + unset($attribs['name']); + $this->current_path = $path; + $this->filelist[$path] = $attribs; + // Set the baseinstalldir only if the file don't have this attrib + if (!isset($this->filelist[$path]['baseinstalldir']) && + isset($this->dir_install)) + { + $this->filelist[$path]['baseinstalldir'] = $this->dir_install; + } + // Set the Role + if (!isset($this->filelist[$path]['role']) && isset($this->dir_role)) { + $this->filelist[$path]['role'] = $this->dir_role; + } + } + break; + case 'replace': + if (!$this->in_changelog) { + $this->filelist[$this->current_path]['replacements'][] = $attribs; + } + break; + case 'maintainers': + $this->_packageInfo['maintainers'] = array(); + $this->m_i = 0; // maintainers array index + break; + case 'maintainer': + // compatibility check + if (!isset($this->_packageInfo['maintainers'])) { + $this->_packageInfo['maintainers'] = array(); + $this->m_i = 0; + } + $this->_packageInfo['maintainers'][$this->m_i] = array(); + $this->current_maintainer =& $this->_packageInfo['maintainers'][$this->m_i]; + break; + case 'changelog': + $this->_packageInfo['changelog'] = array(); + $this->c_i = 0; // changelog array index + $this->in_changelog = true; + break; + case 'release': + if ($this->in_changelog) { + $this->_packageInfo['changelog'][$this->c_i] = array(); + $this->current_release = &$this->_packageInfo['changelog'][$this->c_i]; + } else { + $this->current_release = &$this->_packageInfo; + } + break; + case 'deps': + if (!$this->in_changelog) { + $this->_packageInfo['release_deps'] = array(); + } + break; + case 'dep': + // dependencies array index + if (!$this->in_changelog) { + $this->d_i++; + isset($attribs['type']) ? ($attribs['type'] = strtolower($attribs['type'])) : false; + $this->_packageInfo['release_deps'][$this->d_i] = $attribs; + } + break; + case 'configureoptions': + if (!$this->in_changelog) { + $this->_packageInfo['configure_options'] = array(); + } + break; + case 'configureoption': + if (!$this->in_changelog) { + $this->_packageInfo['configure_options'][] = $attribs; + } + break; + case 'provides': + if (empty($attribs['type']) || empty($attribs['name'])) { + break; + } + $attribs['explicit'] = true; + $this->_packageInfo['provides']["$attribs[type];$attribs[name]"] = $attribs; + break; + case 'package' : + if (isset($attribs['version'])) { + $this->_packageInfo['xsdversion'] = trim($attribs['version']); + } else { + $this->_packageInfo['xsdversion'] = '1.0'; + } + if (isset($attribs['packagerversion'])) { + $this->_packageInfo['packagerversion'] = $attribs['packagerversion']; + } + break; + } + } + + // }}} + // {{{ _element_end_1_0() + + /** + * XML parser callback for ending elements. Used for version 1.0 + * packages. + * + * @param resource $xp XML parser resource + * @param string $name name of ending element + * + * @return void + * + * @access private + */ + function _element_end_1_0($xp, $name) + { + $data = trim($this->cdata); + switch ($name) { + case 'name': + switch ($this->prev_element) { + case 'package': + $this->_packageInfo['package'] = $data; + break; + case 'maintainer': + $this->current_maintainer['name'] = $data; + break; + } + break; + case 'extends' : + $this->_packageInfo['extends'] = $data; + break; + case 'summary': + $this->_packageInfo['summary'] = $data; + break; + case 'description': + $data = $this->_unIndent($this->cdata); + $this->_packageInfo['description'] = $data; + break; + case 'user': + $this->current_maintainer['handle'] = $data; + break; + case 'email': + $this->current_maintainer['email'] = $data; + break; + case 'role': + $this->current_maintainer['role'] = $data; + break; + case 'version': + //$data = ereg_replace ('[^a-zA-Z0-9._\-]', '_', $data); + if ($this->in_changelog) { + $this->current_release['version'] = $data; + } else { + $this->_packageInfo['version'] = $data; + } + break; + case 'date': + if ($this->in_changelog) { + $this->current_release['release_date'] = $data; + } else { + $this->_packageInfo['release_date'] = $data; + } + break; + case 'notes': + // try to "de-indent" release notes in case someone + // has been over-indenting their xml ;-) + $data = $this->_unIndent($this->cdata); + if ($this->in_changelog) { + $this->current_release['release_notes'] = $data; + } else { + $this->_packageInfo['release_notes'] = $data; + } + break; + case 'warnings': + if ($this->in_changelog) { + $this->current_release['release_warnings'] = $data; + } else { + $this->_packageInfo['release_warnings'] = $data; + } + break; + case 'state': + if ($this->in_changelog) { + $this->current_release['release_state'] = $data; + } else { + $this->_packageInfo['release_state'] = $data; + } + break; + case 'license': + if ($this->in_changelog) { + $this->current_release['release_license'] = $data; + } else { + $this->_packageInfo['release_license'] = $data; + } + break; + case 'dep': + if ($data && !$this->in_changelog) { + $this->_packageInfo['release_deps'][$this->d_i]['name'] = $data; + } + break; + case 'dir': + if ($this->in_changelog) { + break; + } + array_pop($this->dir_names); + break; + case 'file': + if ($this->in_changelog) { + break; + } + if ($data) { + $path = ''; + if (count($this->dir_names)) { + foreach ($this->dir_names as $dir) { + $path .= $dir . '/'; + } + } + $path .= $data; + $this->filelist[$path] = $this->current_attributes; + // Set the baseinstalldir only if the file don't have this attrib + if (!isset($this->filelist[$path]['baseinstalldir']) && + isset($this->dir_install)) + { + $this->filelist[$path]['baseinstalldir'] = $this->dir_install; + } + // Set the Role + if (!isset($this->filelist[$path]['role']) && isset($this->dir_role)) { + $this->filelist[$path]['role'] = $this->dir_role; + } + } + break; + case 'maintainer': + if (empty($this->_packageInfo['maintainers'][$this->m_i]['role'])) { + $this->_packageInfo['maintainers'][$this->m_i]['role'] = 'lead'; + } + $this->m_i++; + break; + case 'release': + if ($this->in_changelog) { + $this->c_i++; + } + break; + case 'changelog': + $this->in_changelog = false; + break; + } + array_pop($this->element_stack); + $spos = sizeof($this->element_stack) - 1; + $this->current_element = ($spos > 0) ? $this->element_stack[$spos] : ''; + $this->cdata = ''; + } + + // }}} + // {{{ _pkginfo_cdata_1_0() + + /** + * XML parser callback for character data. Used for version 1.0 + * packages. + * + * @param resource $xp XML parser resource + * @param string $name character data + * + * @return void + * + * @access private + */ + function _pkginfo_cdata_1_0($xp, $data) + { + if (isset($this->cdata)) { + $this->cdata .= $data; + } + } + + // }}} +} +?> \ No newline at end of file diff --git a/PEAR/PackageFile/Parser/v2.php b/PEAR/PackageFile/Parser/v2.php new file mode 100644 index 0000000..879e82f --- /dev/null +++ b/PEAR/PackageFile/Parser/v2.php @@ -0,0 +1,117 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v2.php,v 1.20 2007/06/16 19:13:24 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * base xml parser class + */ +require_once 'PEAR/XMLParser.php'; +require_once 'PEAR/PackageFile/v2.php'; +/** + * Parser for package.xml version 2.0 + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @PEAR-VER@ + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_Parser_v2 extends PEAR_XMLParser +{ + var $_config; + var $_logger; + var $_registry; + + function setConfig(&$c) + { + $this->_config = &$c; + $this->_registry = &$c->getRegistry(); + } + + function setLogger(&$l) + { + $this->_logger = &$l; + } + /** + * Unindent given string + * + * @param string $str The string that has to be unindented. + * @return string + * @access private + */ + function _unIndent($str) + { + // remove leading newlines + $str = preg_replace('/^[\r\n]+/', '', $str); + // find whitespace at the beginning of the first line + $indent_len = strspn($str, " \t"); + $indent = substr($str, 0, $indent_len); + $data = ''; + // remove the same amount of whitespace from following lines + foreach (explode("\n", $str) as $line) { + if (substr($line, 0, $indent_len) == $indent) { + $data .= substr($line, $indent_len) . "\n"; + } else { + $data .= $line . "\n"; + } + } + return $data; + } + + /** + * post-process data + * + * @param string $data + * @param string $element element name + */ + function postProcess($data, $element) + { + if ($element == 'notes') { + return trim($this->_unIndent($data)); + } + return trim($data); + } + + /** + * @param string + * @param string file name of the package.xml + * @param string|false name of the archive this package.xml came from, if any + * @param string class name to instantiate and return. This must be PEAR_PackageFile_v2 or + * a subclass + * @return PEAR_PackageFile_v2 + */ + function &parse($data, $file, $archive = false, $class = 'PEAR_PackageFile_v2') + { + if (PEAR::isError($err = parent::parse($data, $file))) { + return $err; + } + $ret = new $class; + $ret->setConfig($this->_config); + if (isset($this->_logger)) { + $ret->setLogger($this->_logger); + } + $ret->fromArray($this->_unserializedData); + $ret->setPackagefile($file, $archive); + return $ret; + } +} +?> \ No newline at end of file diff --git a/PEAR/PackageFile/v1.php b/PEAR/PackageFile/v1.php new file mode 100644 index 0000000..8390d2a --- /dev/null +++ b/PEAR/PackageFile/v1.php @@ -0,0 +1,1618 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v1.php,v 1.73 2007/05/10 00:00:38 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * For error handling + */ +require_once 'PEAR/ErrorStack.php'; + +/** + * Error code if parsing is attempted with no xml extension + */ +define('PEAR_PACKAGEFILE_ERROR_NO_XML_EXT', 3); + +/** + * Error code if creating the xml parser resource fails + */ +define('PEAR_PACKAGEFILE_ERROR_CANT_MAKE_PARSER', 4); + +/** + * Error code used for all sax xml parsing errors + */ +define('PEAR_PACKAGEFILE_ERROR_PARSER_ERROR', 5); + +/** + * Error code used when there is no name + */ +define('PEAR_PACKAGEFILE_ERROR_NO_NAME', 6); + +/** + * Error code when a package name is not valid + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_NAME', 7); + +/** + * Error code used when no summary is parsed + */ +define('PEAR_PACKAGEFILE_ERROR_NO_SUMMARY', 8); + +/** + * Error code for summaries that are more than 1 line + */ +define('PEAR_PACKAGEFILE_ERROR_MULTILINE_SUMMARY', 9); + +/** + * Error code used when no description is present + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DESCRIPTION', 10); + +/** + * Error code used when no license is present + */ +define('PEAR_PACKAGEFILE_ERROR_NO_LICENSE', 11); + +/** + * Error code used when a version number is not present + */ +define('PEAR_PACKAGEFILE_ERROR_NO_VERSION', 12); + +/** + * Error code used when a version number is invalid + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_VERSION', 13); + +/** + * Error code when release state is missing + */ +define('PEAR_PACKAGEFILE_ERROR_NO_STATE', 14); + +/** + * Error code when release state is invalid + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_STATE', 15); + +/** + * Error code when release state is missing + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DATE', 16); + +/** + * Error code when release state is invalid + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_DATE', 17); + +/** + * Error code when no release notes are found + */ +define('PEAR_PACKAGEFILE_ERROR_NO_NOTES', 18); + +/** + * Error code when no maintainers are found + */ +define('PEAR_PACKAGEFILE_ERROR_NO_MAINTAINERS', 19); + +/** + * Error code when a maintainer has no handle + */ +define('PEAR_PACKAGEFILE_ERROR_NO_MAINTHANDLE', 20); + +/** + * Error code when a maintainer has no handle + */ +define('PEAR_PACKAGEFILE_ERROR_NO_MAINTROLE', 21); + +/** + * Error code when a maintainer has no name + */ +define('PEAR_PACKAGEFILE_ERROR_NO_MAINTNAME', 22); + +/** + * Error code when a maintainer has no email + */ +define('PEAR_PACKAGEFILE_ERROR_NO_MAINTEMAIL', 23); + +/** + * Error code when a maintainer has no handle + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_MAINTROLE', 24); + +/** + * Error code when a dependency is not a PHP dependency, but has no name + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DEPNAME', 25); + +/** + * Error code when a dependency has no type (pkg, php, etc.) + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DEPTYPE', 26); + +/** + * Error code when a dependency has no relation (lt, ge, has, etc.) + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DEPREL', 27); + +/** + * Error code when a dependency is not a 'has' relation, but has no version + */ +define('PEAR_PACKAGEFILE_ERROR_NO_DEPVERSION', 28); + +/** + * Error code when a dependency has an invalid relation + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_DEPREL', 29); + +/** + * Error code when a dependency has an invalid type + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_DEPTYPE', 30); + +/** + * Error code when a dependency has an invalid optional option + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_DEPOPTIONAL', 31); + +/** + * Error code when a dependency is a pkg dependency, and has an invalid package name + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_DEPNAME', 32); + +/** + * Error code when a dependency has a channel="foo" attribute, and foo is not a registered channel + */ +define('PEAR_PACKAGEFILE_ERROR_UNKNOWN_DEPCHANNEL', 33); + +/** + * Error code when rel="has" and version attribute is present. + */ +define('PEAR_PACKAGEFILE_ERROR_DEPVERSION_IGNORED', 34); + +/** + * Error code when type="php" and dependency name is present + */ +define('PEAR_PACKAGEFILE_ERROR_DEPNAME_IGNORED', 35); + +/** + * Error code when a configure option has no name + */ +define('PEAR_PACKAGEFILE_ERROR_NO_CONFNAME', 36); + +/** + * Error code when a configure option has no name + */ +define('PEAR_PACKAGEFILE_ERROR_NO_CONFPROMPT', 37); + +/** + * Error code when a file in the filelist has an invalid role + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_FILEROLE', 38); + +/** + * Error code when a file in the filelist has no role + */ +define('PEAR_PACKAGEFILE_ERROR_NO_FILEROLE', 39); + +/** + * Error code when analyzing a php source file that has parse errors + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_PHPFILE', 40); + +/** + * Error code when analyzing a php source file reveals a source element + * without a package name prefix + */ +define('PEAR_PACKAGEFILE_ERROR_NO_PNAME_PREFIX', 41); + +/** + * Error code when an unknown channel is specified + */ +define('PEAR_PACKAGEFILE_ERROR_UNKNOWN_CHANNEL', 42); + +/** + * Error code when no files are found in the filelist + */ +define('PEAR_PACKAGEFILE_ERROR_NO_FILES', 43); + +/** + * Error code when a file is not valid php according to _analyzeSourceCode() + */ +define('PEAR_PACKAGEFILE_ERROR_INVALID_FILE', 44); + +/** + * Error code when the channel validator returns an error or warning + */ +define('PEAR_PACKAGEFILE_ERROR_CHANNELVAL', 45); + +/** + * Error code when a php5 package is packaged in php4 (analysis doesn't work) + */ +define('PEAR_PACKAGEFILE_ERROR_PHP5', 46); + +/** + * Error code when a file is listed in package.xml but does not exist + */ +define('PEAR_PACKAGEFILE_ERROR_FILE_NOTFOUND', 47); + +/** + * Error code when a + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_v1 +{ + /** + * @access private + * @var PEAR_ErrorStack + * @access private + */ + var $_stack; + + /** + * A registry object, used to access the package name validation regex for non-standard channels + * @var PEAR_Registry + * @access private + */ + var $_registry; + + /** + * An object that contains a log method that matches PEAR_Common::log's signature + * @var object + * @access private + */ + var $_logger; + + /** + * Parsed package information + * @var array + * @access private + */ + var $_packageInfo; + + /** + * path to package.xml + * @var string + * @access private + */ + var $_packageFile; + + /** + * path to package .tgz or false if this is a local/extracted package.xml + * @var string + * @access private + */ + var $_archiveFile; + + /** + * @var int + * @access private + */ + var $_isValid = 0; + + /** + * Determines whether this packagefile was initialized only with partial package info + * + * If this package file was constructed via parsing REST, it will only contain + * + * - package name + * - channel name + * - dependencies + * @var boolean + * @access private + */ + var $_incomplete = true; + + /** + * @param bool determines whether to return a PEAR_Error object, or use the PEAR_ErrorStack + * @param string Name of Error Stack class to use. + */ + function PEAR_PackageFile_v1() + { + $this->_stack = &new PEAR_ErrorStack('PEAR_PackageFile_v1'); + $this->_stack->setErrorMessageTemplate($this->_getErrorMessage()); + $this->_isValid = 0; + } + + function installBinary($installer) + { + return false; + } + + function isExtension($name) + { + return false; + } + + function setConfig(&$config) + { + $this->_config = &$config; + $this->_registry = &$config->getRegistry(); + } + + function setRequestedGroup() + { + // placeholder + } + + /** + * For saving in the registry. + * + * Set the last version that was installed + * @param string + */ + function setLastInstalledVersion($version) + { + $this->_packageInfo['_lastversion'] = $version; + } + + /** + * @return string|false + */ + function getLastInstalledVersion() + { + if (isset($this->_packageInfo['_lastversion'])) { + return $this->_packageInfo['_lastversion']; + } + return false; + } + + function getInstalledBinary() + { + return false; + } + + function listPostinstallScripts() + { + return false; + } + + function initPostinstallScripts() + { + return false; + } + + function setLogger(&$logger) + { + if ($logger && (!is_object($logger) || !method_exists($logger, 'log'))) { + return PEAR::raiseError('Logger must be compatible with PEAR_Common::log'); + } + $this->_logger = &$logger; + } + + function setPackagefile($file, $archive = false) + { + $this->_packageFile = $file; + $this->_archiveFile = $archive ? $archive : $file; + } + + function getPackageFile() + { + return isset($this->_packageFile) ? $this->_packageFile : false; + } + + function getPackageType() + { + return 'php'; + } + + function getArchiveFile() + { + return $this->_archiveFile; + } + + function packageInfo($field) + { + if (!is_string($field) || empty($field) || + !isset($this->_packageInfo[$field])) { + return false; + } + return $this->_packageInfo[$field]; + } + + function setDirtree($path) + { + if (!isset($this->_packageInfo['dirtree'])) { + $this->_packageInfo['dirtree'] = array(); + } + $this->_packageInfo['dirtree'][$path] = true; + } + + function getDirtree() + { + if (isset($this->_packageInfo['dirtree']) && count($this->_packageInfo['dirtree'])) { + return $this->_packageInfo['dirtree']; + } + return false; + } + + function resetDirtree() + { + unset($this->_packageInfo['dirtree']); + } + + function fromArray($pinfo) + { + $this->_incomplete = false; + $this->_packageInfo = $pinfo; + } + + function isIncomplete() + { + return $this->_incomplete; + } + + function getChannel() + { + return 'pear.php.net'; + } + + function getUri() + { + return false; + } + + function getTime() + { + return false; + } + + function getExtends() + { + if (isset($this->_packageInfo['extends'])) { + return $this->_packageInfo['extends']; + } + return false; + } + + /** + * @return array + */ + function toArray() + { + if (!$this->validate(PEAR_VALIDATE_NORMAL)) { + return false; + } + return $this->getArray(); + } + + function getArray() + { + return $this->_packageInfo; + } + + function getName() + { + return $this->getPackage(); + } + + function getPackage() + { + if (isset($this->_packageInfo['package'])) { + return $this->_packageInfo['package']; + } + return false; + } + + /** + * WARNING - don't use this unless you know what you are doing + */ + function setRawPackage($package) + { + $this->_packageInfo['package'] = $package; + } + + function setPackage($package) + { + $this->_packageInfo['package'] = $package; + $this->_isValid = false; + } + + function getVersion() + { + if (isset($this->_packageInfo['version'])) { + return $this->_packageInfo['version']; + } + return false; + } + + function setVersion($version) + { + $this->_packageInfo['version'] = $version; + $this->_isValid = false; + } + + function clearMaintainers() + { + unset($this->_packageInfo['maintainers']); + } + + function getMaintainers() + { + if (isset($this->_packageInfo['maintainers'])) { + return $this->_packageInfo['maintainers']; + } + return false; + } + + /** + * Adds a new maintainer - no checking of duplicates is performed, use + * updatemaintainer for that purpose. + */ + function addMaintainer($role, $handle, $name, $email) + { + $this->_packageInfo['maintainers'][] = + array('handle' => $handle, 'role' => $role, 'email' => $email, 'name' => $name); + $this->_isValid = false; + } + + function updateMaintainer($role, $handle, $name, $email) + { + $found = false; + if (!isset($this->_packageInfo['maintainers']) || + !is_array($this->_packageInfo['maintainers'])) { + return $this->addMaintainer($role, $handle, $name, $email); + } + foreach ($this->_packageInfo['maintainers'] as $i => $maintainer) { + if ($maintainer['handle'] == $handle) { + $found = $i; + break; + } + } + if ($found !== false) { + unset($this->_packageInfo['maintainers'][$found]); + $this->_packageInfo['maintainers'] = + array_values($this->_packageInfo['maintainers']); + } + $this->addMaintainer($role, $handle, $name, $email); + } + + function deleteMaintainer($handle) + { + $found = false; + foreach ($this->_packageInfo['maintainers'] as $i => $maintainer) { + if ($maintainer['handle'] == $handle) { + $found = $i; + break; + } + } + if ($found !== false) { + unset($this->_packageInfo['maintainers'][$found]); + $this->_packageInfo['maintainers'] = + array_values($this->_packageInfo['maintainers']); + return true; + } + return false; + } + + function getState() + { + if (isset($this->_packageInfo['release_state'])) { + return $this->_packageInfo['release_state']; + } + return false; + } + + function setRawState($state) + { + $this->_packageInfo['release_state'] = $state; + } + + function setState($state) + { + $this->_packageInfo['release_state'] = $state; + $this->_isValid = false; + } + + function getDate() + { + if (isset($this->_packageInfo['release_date'])) { + return $this->_packageInfo['release_date']; + } + return false; + } + + function setDate($date) + { + $this->_packageInfo['release_date'] = $date; + $this->_isValid = false; + } + + function getLicense() + { + if (isset($this->_packageInfo['release_license'])) { + return $this->_packageInfo['release_license']; + } + return false; + } + + function setLicense($date) + { + $this->_packageInfo['release_license'] = $date; + $this->_isValid = false; + } + + function getSummary() + { + if (isset($this->_packageInfo['summary'])) { + return $this->_packageInfo['summary']; + } + return false; + } + + function setSummary($summary) + { + $this->_packageInfo['summary'] = $summary; + $this->_isValid = false; + } + + function getDescription() + { + if (isset($this->_packageInfo['description'])) { + return $this->_packageInfo['description']; + } + return false; + } + + function setDescription($desc) + { + $this->_packageInfo['description'] = $desc; + $this->_isValid = false; + } + + function getNotes() + { + if (isset($this->_packageInfo['release_notes'])) { + return $this->_packageInfo['release_notes']; + } + return false; + } + + function setNotes($notes) + { + $this->_packageInfo['release_notes'] = $notes; + $this->_isValid = false; + } + + function getDeps() + { + if (isset($this->_packageInfo['release_deps'])) { + return $this->_packageInfo['release_deps']; + } + return false; + } + + /** + * Reset dependencies prior to adding new ones + */ + function clearDeps() + { + unset($this->_packageInfo['release_deps']); + } + + function addPhpDep($version, $rel) + { + $this->_isValid = false; + $this->_packageInfo['release_deps'][] = + array('type' => 'php', + 'rel' => $rel, + 'version' => $version); + } + + function addPackageDep($name, $version, $rel, $optional = 'no') + { + $this->_isValid = false; + $dep = + array('type' => 'pkg', + 'name' => $name, + 'rel' => $rel, + 'optional' => $optional); + if ($rel != 'has' && $rel != 'not') { + $dep['version'] = $version; + } + $this->_packageInfo['release_deps'][] = $dep; + } + + function addExtensionDep($name, $version, $rel, $optional = 'no') + { + $this->_isValid = false; + $this->_packageInfo['release_deps'][] = + array('type' => 'ext', + 'name' => $name, + 'rel' => $rel, + 'version' => $version, + 'optional' => $optional); + } + + /** + * WARNING - do not use this function directly unless you know what you're doing + */ + function setDeps($deps) + { + $this->_packageInfo['release_deps'] = $deps; + } + + function hasDeps() + { + return isset($this->_packageInfo['release_deps']) && + count($this->_packageInfo['release_deps']); + } + + function getDependencyGroup($group) + { + return false; + } + + function isCompatible($pf) + { + return false; + } + + function isSubpackageOf($p) + { + return $p->isSubpackage($this); + } + + function isSubpackage($p) + { + return false; + } + + function dependsOn($package, $channel) + { + if (strtolower($channel) != 'pear.php.net') { + return false; + } + if (!($deps = $this->getDeps())) { + return false; + } + foreach ($deps as $dep) { + if ($dep['type'] != 'pkg') { + continue; + } + if (strtolower($dep['name']) == strtolower($package)) { + return true; + } + } + return false; + } + + function getConfigureOptions() + { + if (isset($this->_packageInfo['configure_options'])) { + return $this->_packageInfo['configure_options']; + } + return false; + } + + function hasConfigureOptions() + { + return isset($this->_packageInfo['configure_options']) && + count($this->_packageInfo['configure_options']); + } + + function addConfigureOption($name, $prompt, $default = false) + { + $o = array('name' => $name, 'prompt' => $prompt); + if ($default !== false) { + $o['default'] = $default; + } + if (!isset($this->_packageInfo['configure_options'])) { + $this->_packageInfo['configure_options'] = array(); + } + $this->_packageInfo['configure_options'][] = $o; + } + + function clearConfigureOptions() + { + unset($this->_packageInfo['configure_options']); + } + + function getProvides() + { + if (isset($this->_packageInfo['provides'])) { + return $this->_packageInfo['provides']; + } + return false; + } + + function getProvidesExtension() + { + return false; + } + + function addFile($dir, $file, $attrs) + { + $dir = preg_replace(array('!\\\\+!', '!/+!'), array('/', '/'), $dir); + if ($dir == '/' || $dir == '') { + $dir = ''; + } else { + $dir .= '/'; + } + $file = $dir . $file; + $file = preg_replace('![\\/]+!', '/', $file); + $this->_packageInfo['filelist'][$file] = $attrs; + } + + function getInstallationFilelist() + { + return $this->getFilelist(); + } + + function getFilelist() + { + if (isset($this->_packageInfo['filelist'])) { + return $this->_packageInfo['filelist']; + } + return false; + } + + function setFileAttribute($file, $attr, $value) + { + $this->_packageInfo['filelist'][$file][$attr] = $value; + } + + function resetFilelist() + { + $this->_packageInfo['filelist'] = array(); + } + + function setInstalledAs($file, $path) + { + if ($path) { + return $this->_packageInfo['filelist'][$file]['installed_as'] = $path; + } + unset($this->_packageInfo['filelist'][$file]['installed_as']); + } + + function installedFile($file, $atts) + { + if (isset($this->_packageInfo['filelist'][$file])) { + $this->_packageInfo['filelist'][$file] = + array_merge($this->_packageInfo['filelist'][$file], $atts); + } else { + $this->_packageInfo['filelist'][$file] = $atts; + } + } + + function getChangelog() + { + if (isset($this->_packageInfo['changelog'])) { + return $this->_packageInfo['changelog']; + } + return false; + } + + function getPackagexmlVersion() + { + return '1.0'; + } + + /** + * Wrapper to {@link PEAR_ErrorStack::getErrors()} + * @param boolean determines whether to purge the error stack after retrieving + * @return array + */ + function getValidationWarnings($purge = true) + { + return $this->_stack->getErrors($purge); + } + + // }}} + /** + * Validation error. Also marks the object contents as invalid + * @param error code + * @param array error information + * @access private + */ + function _validateError($code, $params = array()) + { + $this->_stack->push($code, 'error', $params, false, false, debug_backtrace()); + $this->_isValid = false; + } + + /** + * Validation warning. Does not mark the object contents invalid. + * @param error code + * @param array error information + * @access private + */ + function _validateWarning($code, $params = array()) + { + $this->_stack->push($code, 'warning', $params, false, false, debug_backtrace()); + } + + /** + * @param integer error code + * @access protected + */ + function _getErrorMessage() + { + return array( + PEAR_PACKAGEFILE_ERROR_NO_NAME => + 'Missing Package Name', + PEAR_PACKAGEFILE_ERROR_NO_SUMMARY => + 'No summary found', + PEAR_PACKAGEFILE_ERROR_MULTILINE_SUMMARY => + 'Summary should be on one line', + PEAR_PACKAGEFILE_ERROR_NO_DESCRIPTION => + 'Missing description', + PEAR_PACKAGEFILE_ERROR_NO_LICENSE => + 'Missing license', + PEAR_PACKAGEFILE_ERROR_NO_VERSION => + 'No release version found', + PEAR_PACKAGEFILE_ERROR_NO_STATE => + 'No release state found', + PEAR_PACKAGEFILE_ERROR_NO_DATE => + 'No release date found', + PEAR_PACKAGEFILE_ERROR_NO_NOTES => + 'No release notes found', + PEAR_PACKAGEFILE_ERROR_NO_LEAD => + 'Package must have at least one lead maintainer', + PEAR_PACKAGEFILE_ERROR_NO_MAINTAINERS => + 'No maintainers found, at least one must be defined', + PEAR_PACKAGEFILE_ERROR_NO_MAINTHANDLE => + 'Maintainer %index% has no handle (user ID at channel server)', + PEAR_PACKAGEFILE_ERROR_NO_MAINTROLE => + 'Maintainer %index% has no role', + PEAR_PACKAGEFILE_ERROR_NO_MAINTNAME => + 'Maintainer %index% has no name', + PEAR_PACKAGEFILE_ERROR_NO_MAINTEMAIL => + 'Maintainer %index% has no email', + PEAR_PACKAGEFILE_ERROR_NO_DEPNAME => + 'Dependency %index% is not a php dependency, and has no name', + PEAR_PACKAGEFILE_ERROR_NO_DEPREL => + 'Dependency %index% has no relation (rel)', + PEAR_PACKAGEFILE_ERROR_NO_DEPTYPE => + 'Dependency %index% has no type', + PEAR_PACKAGEFILE_ERROR_DEPNAME_IGNORED => + 'PHP Dependency %index% has a name attribute of "%name%" which will be' . + ' ignored!', + PEAR_PACKAGEFILE_ERROR_NO_DEPVERSION => + 'Dependency %index% is not a rel="has" or rel="not" dependency, ' . + 'and has no version', + PEAR_PACKAGEFILE_ERROR_NO_DEPPHPVERSION => + 'Dependency %index% is a type="php" dependency, ' . + 'and has no version', + PEAR_PACKAGEFILE_ERROR_DEPVERSION_IGNORED => + 'Dependency %index% is a rel="%rel%" dependency, versioning is ignored', + PEAR_PACKAGEFILE_ERROR_INVALID_DEPOPTIONAL => + 'Dependency %index% has invalid optional value "%opt%", should be yes or no', + PEAR_PACKAGEFILE_PHP_NO_NOT => + 'Dependency %index%: php dependencies cannot use "not" rel, use "ne"' . + ' to exclude specific versions', + PEAR_PACKAGEFILE_ERROR_NO_CONFNAME => + 'Configure Option %index% has no name', + PEAR_PACKAGEFILE_ERROR_NO_CONFPROMPT => + 'Configure Option %index% has no prompt', + PEAR_PACKAGEFILE_ERROR_NO_FILES => + 'No files in section of package.xml', + PEAR_PACKAGEFILE_ERROR_NO_FILEROLE => + 'File "%file%" has no role, expecting one of "%roles%"', + PEAR_PACKAGEFILE_ERROR_INVALID_FILEROLE => + 'File "%file%" has invalid role "%role%", expecting one of "%roles%"', + PEAR_PACKAGEFILE_ERROR_INVALID_FILENAME => + 'File "%file%" cannot start with ".", cannot package or install', + PEAR_PACKAGEFILE_ERROR_INVALID_PHPFILE => + 'Parser error: invalid PHP found in file "%file%"', + PEAR_PACKAGEFILE_ERROR_NO_PNAME_PREFIX => + 'in %file%: %type% "%name%" not prefixed with package name "%package%"', + PEAR_PACKAGEFILE_ERROR_INVALID_FILE => + 'Parser error: invalid PHP file "%file%"', + PEAR_PACKAGEFILE_ERROR_CHANNELVAL => + 'Channel validator error: field "%field%" - %reason%', + PEAR_PACKAGEFILE_ERROR_PHP5 => + 'Error, PHP5 token encountered in %file%, analysis should be in PHP5', + PEAR_PACKAGEFILE_ERROR_FILE_NOTFOUND => + 'File "%file%" in package.xml does not exist', + PEAR_PACKAGEFILE_ERROR_NON_ISO_CHARS => + 'Package.xml contains non-ISO-8859-1 characters, and may not validate', + ); + } + + /** + * Validate XML package definition file. + * + * @access public + * @return boolean + */ + function validate($state = PEAR_VALIDATE_NORMAL, $nofilechecking = false) + { + if (($this->_isValid & $state) == $state) { + return true; + } + $this->_isValid = true; + $info = $this->_packageInfo; + if (empty($info['package'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_NAME); + $this->_packageName = $pn = 'unknown'; + } else { + $this->_packageName = $pn = $info['package']; + } + + if (empty($info['summary'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_SUMMARY); + } elseif (strpos(trim($info['summary']), "\n") !== false) { + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_MULTILINE_SUMMARY, + array('summary' => $info['summary'])); + } + if (empty($info['description'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DESCRIPTION); + } + if (empty($info['release_license'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_LICENSE); + } + if (empty($info['version'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_VERSION); + } + if (empty($info['release_state'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_STATE); + } + if (empty($info['release_date'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DATE); + } + if (empty($info['release_notes'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_NOTES); + } + if (empty($info['maintainers'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_MAINTAINERS); + } else { + $haslead = false; + $i = 1; + foreach ($info['maintainers'] as $m) { + if (empty($m['handle'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_MAINTHANDLE, + array('index' => $i)); + } + if (empty($m['role'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_MAINTROLE, + array('index' => $i, 'roles' => PEAR_Common::getUserRoles())); + } elseif ($m['role'] == 'lead') { + $haslead = true; + } + if (empty($m['name'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_MAINTNAME, + array('index' => $i)); + } + if (empty($m['email'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_MAINTEMAIL, + array('index' => $i)); + } + $i++; + } + if (!$haslead) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_LEAD); + } + } + if (!empty($info['release_deps'])) { + $i = 1; + foreach ($info['release_deps'] as $d) { + if (!isset($d['type']) || empty($d['type'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DEPTYPE, + array('index' => $i, 'types' => PEAR_Common::getDependencyTypes())); + continue; + } + if (!isset($d['rel']) || empty($d['rel'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DEPREL, + array('index' => $i, 'rels' => PEAR_Common::getDependencyRelations())); + continue; + } + if (!empty($d['optional'])) { + if (!in_array($d['optional'], array('yes', 'no'))) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_DEPOPTIONAL, + array('index' => $i, 'opt' => $d['optional'])); + } + } + if ($d['rel'] != 'has' && $d['rel'] != 'not' && empty($d['version'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DEPVERSION, + array('index' => $i)); + } elseif (($d['rel'] == 'has' || $d['rel'] == 'not') && !empty($d['version'])) { + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_DEPVERSION_IGNORED, + array('index' => $i, 'rel' => $d['rel'])); + } + if ($d['type'] == 'php' && !empty($d['name'])) { + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_DEPNAME_IGNORED, + array('index' => $i, 'name' => $d['name'])); + } elseif ($d['type'] != 'php' && empty($d['name'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DEPNAME, + array('index' => $i)); + } + if ($d['type'] == 'php' && empty($d['version'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_DEPPHPVERSION, + array('index' => $i)); + } + if (($d['rel'] == 'not') && ($d['type'] == 'php')) { + $this->_validateError(PEAR_PACKAGEFILE_PHP_NO_NOT, + array('index' => $i)); + } + $i++; + } + } + if (!empty($info['configure_options'])) { + $i = 1; + foreach ($info['configure_options'] as $c) { + if (empty($c['name'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_CONFNAME, + array('index' => $i)); + } + if (empty($c['prompt'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_CONFPROMPT, + array('index' => $i)); + } + $i++; + } + } + if (empty($info['filelist'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_FILES); + $errors[] = 'no files'; + } else { + foreach ($info['filelist'] as $file => $fa) { + if (empty($fa['role'])) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_NO_FILEROLE, + array('file' => $file, 'roles' => PEAR_Common::getFileRoles())); + continue; + } elseif (!in_array($fa['role'], PEAR_Common::getFileRoles())) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_FILEROLE, + array('file' => $file, 'role' => $fa['role'], 'roles' => PEAR_Common::getFileRoles())); + } + if (preg_match('~/\.\.?(/|\\z)|^\.\.?/~', str_replace('\\', '/', $file))) { + // file contains .. parent directory or . cur directory references + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_FILENAME, + array('file' => $file)); + } + if (isset($fa['install-as']) && + preg_match('~/\.\.?(/|\\z)|^\.\.?/~', + str_replace('\\', '/', $fa['install-as']))) { + // install-as contains .. parent directory or . cur directory references + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_FILENAME, + array('file' => $file . ' [installed as ' . $fa['install-as'] . ']')); + } + if (isset($fa['baseinstalldir']) && + preg_match('~/\.\.?(/|\\z)|^\.\.?/~', + str_replace('\\', '/', $fa['baseinstalldir']))) { + // install-as contains .. parent directory or . cur directory references + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_FILENAME, + array('file' => $file . ' [baseinstalldir ' . $fa['baseinstalldir'] . ']')); + } + } + } + if (isset($this->_registry) && $this->_isValid) { + $chan = $this->_registry->getChannel('pear.php.net'); + if (PEAR::isError($chan)) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_CHANNELVAL, $chan->getMessage()); + return $this->_isValid = 0; + } + $validator = $chan->getValidationObject(); + $validator->setPackageFile($this); + $validator->validate($state); + $failures = $validator->getFailures(); + foreach ($failures['errors'] as $error) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_CHANNELVAL, $error); + } + foreach ($failures['warnings'] as $warning) { + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_CHANNELVAL, $warning); + } + } + if ($this->_isValid && $state == PEAR_VALIDATE_PACKAGING && !$nofilechecking) { + if ($this->_analyzePhpFiles()) { + $this->_isValid = true; + } + } + if ($this->_isValid) { + return $this->_isValid = $state; + } + return $this->_isValid = 0; + } + + function _analyzePhpFiles() + { + if (!$this->_isValid) { + return false; + } + if (!isset($this->_packageFile)) { + return false; + } + $dir_prefix = dirname($this->_packageFile); + $common = new PEAR_Common; + $log = isset($this->_logger) ? array(&$this->_logger, 'log') : + array($common, 'log'); + $info = $this->getFilelist(); + foreach ($info as $file => $fa) { + if (!file_exists($dir_prefix . DIRECTORY_SEPARATOR . $file)) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_FILE_NOTFOUND, + array('file' => realpath($dir_prefix) . DIRECTORY_SEPARATOR . $file)); + continue; + } + if ($fa['role'] == 'php' && $dir_prefix) { + call_user_func_array($log, array(1, "Analyzing $file")); + $srcinfo = $this->_analyzeSourceCode($dir_prefix . DIRECTORY_SEPARATOR . $file); + if ($srcinfo) { + $this->_buildProvidesArray($srcinfo); + } + } + } + $this->_packageName = $pn = $this->getPackage(); + $pnl = strlen($pn); + if (isset($this->_packageInfo['provides'])) { + foreach ((array) $this->_packageInfo['provides'] as $key => $what) { + if (isset($what['explicit'])) { + // skip conformance checks if the provides entry is + // specified in the package.xml file + continue; + } + extract($what); + if ($type == 'class') { + if (!strncasecmp($name, $pn, $pnl)) { + continue; + } + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_NO_PNAME_PREFIX, + array('file' => $file, 'type' => $type, 'name' => $name, 'package' => $pn)); + } elseif ($type == 'function') { + if (strstr($name, '::') || !strncasecmp($name, $pn, $pnl)) { + continue; + } + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_NO_PNAME_PREFIX, + array('file' => $file, 'type' => $type, 'name' => $name, 'package' => $pn)); + } + } + } + return $this->_isValid; + } + + /** + * Get the default xml generator object + * + * @return PEAR_PackageFile_Generator_v1 + */ + function &getDefaultGenerator() + { + if (!class_exists('PEAR_PackageFile_Generator_v1')) { + require_once 'PEAR/PackageFile/Generator/v1.php'; + } + $a = &new PEAR_PackageFile_Generator_v1($this); + return $a; + } + + /** + * Get the contents of a file listed within the package.xml + * @param string + * @return string + */ + function getFileContents($file) + { + if ($this->_archiveFile == $this->_packageFile) { // unpacked + $dir = dirname($this->_packageFile); + $file = $dir . DIRECTORY_SEPARATOR . $file; + $file = str_replace(array('/', '\\'), + array(DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $file); + if (file_exists($file) && is_readable($file)) { + return implode('', file($file)); + } + } else { // tgz + if (!class_exists('Archive_Tar')) { + require_once 'Archive/Tar.php'; + } + $tar = &new Archive_Tar($this->_archiveFile); + $tar->pushErrorHandling(PEAR_ERROR_RETURN); + if ($file != 'package.xml' && $file != 'package2.xml') { + $file = $this->getPackage() . '-' . $this->getVersion() . '/' . $file; + } + $file = $tar->extractInString($file); + $tar->popErrorHandling(); + if (PEAR::isError($file)) { + return PEAR::raiseError("Cannot locate file '$file' in archive"); + } + return $file; + } + } + + // {{{ analyzeSourceCode() + /** + * Analyze the source code of the given PHP file + * + * @param string Filename of the PHP file + * @return mixed + * @access private + */ + function _analyzeSourceCode($file) + { + if (!function_exists("token_get_all")) { + return false; + } + if (!defined('T_DOC_COMMENT')) { + define('T_DOC_COMMENT', T_COMMENT); + } + if (!defined('T_INTERFACE')) { + define('T_INTERFACE', -1); + } + if (!defined('T_IMPLEMENTS')) { + define('T_IMPLEMENTS', -1); + } + if (!$fp = @fopen($file, "r")) { + return false; + } + fclose($fp); + $contents = file_get_contents($file); + $tokens = token_get_all($contents); +/* + for ($i = 0; $i < sizeof($tokens); $i++) { + @list($token, $data) = $tokens[$i]; + if (is_string($token)) { + var_dump($token); + } else { + print token_name($token) . ' '; + var_dump(rtrim($data)); + } + } +*/ + $look_for = 0; + $paren_level = 0; + $bracket_level = 0; + $brace_level = 0; + $lastphpdoc = ''; + $current_class = ''; + $current_interface = ''; + $current_class_level = -1; + $current_function = ''; + $current_function_level = -1; + $declared_classes = array(); + $declared_interfaces = array(); + $declared_functions = array(); + $declared_methods = array(); + $used_classes = array(); + $used_functions = array(); + $extends = array(); + $implements = array(); + $nodeps = array(); + $inquote = false; + $interface = false; + for ($i = 0; $i < sizeof($tokens); $i++) { + if (is_array($tokens[$i])) { + list($token, $data) = $tokens[$i]; + } else { + $token = $tokens[$i]; + $data = ''; + } + if ($inquote) { + if ($token != '"' && $token != T_END_HEREDOC) { + continue; + } else { + $inquote = false; + continue; + } + } + switch ($token) { + case T_WHITESPACE : + continue; + case ';': + if ($interface) { + $current_function = ''; + $current_function_level = -1; + } + break; + case '"': + case T_START_HEREDOC: + $inquote = true; + break; + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + case '{': $brace_level++; continue 2; + case '}': + $brace_level--; + if ($current_class_level == $brace_level) { + $current_class = ''; + $current_class_level = -1; + } + if ($current_function_level == $brace_level) { + $current_function = ''; + $current_function_level = -1; + } + continue 2; + case '[': $bracket_level++; continue 2; + case ']': $bracket_level--; continue 2; + case '(': $paren_level++; continue 2; + case ')': $paren_level--; continue 2; + case T_INTERFACE: + $interface = true; + case T_CLASS: + if (($current_class_level != -1) || ($current_function_level != -1)) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_PHPFILE, + array('file' => $file)); + return false; + } + case T_FUNCTION: + case T_NEW: + case T_EXTENDS: + case T_IMPLEMENTS: + $look_for = $token; + continue 2; + case T_STRING: + if (version_compare(zend_version(), '2.0', '<')) { + if (in_array(strtolower($data), + array('public', 'private', 'protected', 'abstract', + 'interface', 'implements', 'throw') + )) { + $this->_validateWarning(PEAR_PACKAGEFILE_ERROR_PHP5, + array($file)); + } + } + if ($look_for == T_CLASS) { + $current_class = $data; + $current_class_level = $brace_level; + $declared_classes[] = $current_class; + } elseif ($look_for == T_INTERFACE) { + $current_interface = $data; + $current_class_level = $brace_level; + $declared_interfaces[] = $current_interface; + } elseif ($look_for == T_IMPLEMENTS) { + $implements[$current_class] = $data; + } elseif ($look_for == T_EXTENDS) { + $extends[$current_class] = $data; + } elseif ($look_for == T_FUNCTION) { + if ($current_class) { + $current_function = "$current_class::$data"; + $declared_methods[$current_class][] = $data; + } elseif ($current_interface) { + $current_function = "$current_interface::$data"; + $declared_methods[$current_interface][] = $data; + } else { + $current_function = $data; + $declared_functions[] = $current_function; + } + $current_function_level = $brace_level; + $m = array(); + } elseif ($look_for == T_NEW) { + $used_classes[$data] = true; + } + $look_for = 0; + continue 2; + case T_VARIABLE: + $look_for = 0; + continue 2; + case T_DOC_COMMENT: + case T_COMMENT: + if (preg_match('!^/\*\*\s!', $data)) { + $lastphpdoc = $data; + if (preg_match_all('/@nodep\s+(\S+)/', $lastphpdoc, $m)) { + $nodeps = array_merge($nodeps, $m[1]); + } + } + continue 2; + case T_DOUBLE_COLON: + if (!($tokens[$i - 1][0] == T_WHITESPACE || $tokens[$i - 1][0] == T_STRING)) { + $this->_validateError(PEAR_PACKAGEFILE_ERROR_INVALID_PHPFILE, + array('file' => $file)); + return false; + } + $class = $tokens[$i - 1][1]; + if (strtolower($class) != 'parent') { + $used_classes[$class] = true; + } + continue 2; + } + } + return array( + "source_file" => $file, + "declared_classes" => $declared_classes, + "declared_interfaces" => $declared_interfaces, + "declared_methods" => $declared_methods, + "declared_functions" => $declared_functions, + "used_classes" => array_diff(array_keys($used_classes), $nodeps), + "inheritance" => $extends, + "implements" => $implements, + ); + } + + /** + * Build a "provides" array from data returned by + * analyzeSourceCode(). The format of the built array is like + * this: + * + * array( + * 'class;MyClass' => 'array('type' => 'class', 'name' => 'MyClass'), + * ... + * ) + * + * + * @param array $srcinfo array with information about a source file + * as returned by the analyzeSourceCode() method. + * + * @return void + * + * @access private + * + */ + function _buildProvidesArray($srcinfo) + { + if (!$this->_isValid) { + return false; + } + $file = basename($srcinfo['source_file']); + $pn = $this->getPackage(); + $pnl = strlen($pn); + foreach ($srcinfo['declared_classes'] as $class) { + $key = "class;$class"; + if (isset($this->_packageInfo['provides'][$key])) { + continue; + } + $this->_packageInfo['provides'][$key] = + array('file'=> $file, 'type' => 'class', 'name' => $class); + if (isset($srcinfo['inheritance'][$class])) { + $this->_packageInfo['provides'][$key]['extends'] = + $srcinfo['inheritance'][$class]; + } + } + foreach ($srcinfo['declared_methods'] as $class => $methods) { + foreach ($methods as $method) { + $function = "$class::$method"; + $key = "function;$function"; + if ($method{0} == '_' || !strcasecmp($method, $class) || + isset($this->_packageInfo['provides'][$key])) { + continue; + } + $this->_packageInfo['provides'][$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + } + + foreach ($srcinfo['declared_functions'] as $function) { + $key = "function;$function"; + if ($function{0} == '_' || isset($this->_packageInfo['provides'][$key])) { + continue; + } + if (!strstr($function, '::') && strncasecmp($function, $pn, $pnl)) { + $warnings[] = "in1 " . $file . ": function \"$function\" not prefixed with package name \"$pn\""; + } + $this->_packageInfo['provides'][$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + } + + // }}} +} +?> diff --git a/PEAR/PackageFile/v2.php b/PEAR/PackageFile/v2.php new file mode 100644 index 0000000..12c15ae --- /dev/null +++ b/PEAR/PackageFile/v2.php @@ -0,0 +1,2042 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: v2.php,v 1.140 2007/06/03 04:22:13 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * For error handling + */ +require_once 'PEAR/ErrorStack.php'; +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_PackageFile_v2 +{ + + /** + * Parsed package information + * @var array + * @access private + */ + var $_packageInfo = array(); + + /** + * path to package .tgz or false if this is a local/extracted package.xml + * @var string|false + * @access private + */ + var $_archiveFile; + + /** + * path to package .xml or false if this is an abstract parsed-from-string xml + * @var string|false + * @access private + */ + var $_packageFile; + + /** + * This is used by file analysis routines to log progress information + * @var PEAR_Common + * @access protected + */ + var $_logger; + + /** + * This is set to the highest validation level that has been validated + * + * If the package.xml is invalid or unknown, this is set to 0. If + * normal validation has occurred, this is set to PEAR_VALIDATE_NORMAL. If + * downloading/installation validation has occurred it is set to PEAR_VALIDATE_DOWNLOADING + * or INSTALLING, and so on up to PEAR_VALIDATE_PACKAGING. This allows validation + * "caching" to occur, which is particularly important for package validation, so + * that PHP files are not validated twice + * @var int + * @access private + */ + var $_isValid = 0; + + /** + * True if the filelist has been validated + * @param bool + */ + var $_filesValid = false; + + /** + * @var PEAR_Registry + * @access protected + */ + var $_registry; + + /** + * @var PEAR_Config + * @access protected + */ + var $_config; + + /** + * Optional Dependency group requested for installation + * @var string + * @access private + */ + var $_requestedGroup = false; + + /** + * @var PEAR_ErrorStack + * @access protected + */ + var $_stack; + + /** + * Namespace prefix used for tasks in this package.xml - use tasks: whenever possible + */ + var $_tasksNs; + + /** + * Determines whether this packagefile was initialized only with partial package info + * + * If this package file was constructed via parsing REST, it will only contain + * + * - package name + * - channel name + * - dependencies + * @var boolean + * @access private + */ + var $_incomplete = true; + + /** + * @var PEAR_PackageFile_v2_Validator + */ + var $_v2Validator; + + /** + * The constructor merely sets up the private error stack + */ + function PEAR_PackageFile_v2() + { + $this->_stack = new PEAR_ErrorStack('PEAR_PackageFile_v2', false, null); + $this->_isValid = false; + } + + /** + * To make unit-testing easier + * @param PEAR_Frontend_* + * @param array options + * @param PEAR_Config + * @return PEAR_Downloader + * @access protected + */ + function &getPEARDownloader(&$i, $o, &$c) + { + $z = &new PEAR_Downloader($i, $o, $c); + return $z; + } + + /** + * To make unit-testing easier + * @param PEAR_Config + * @param array options + * @param array package name as returned from {@link PEAR_Registry::parsePackageName()} + * @param int PEAR_VALIDATE_* constant + * @return PEAR_Dependency2 + * @access protected + */ + function &getPEARDependency2(&$c, $o, $p, $s = PEAR_VALIDATE_INSTALLING) + { + if (!class_exists('PEAR_Dependency2')) { + require_once 'PEAR/Dependency2.php'; + } + $z = &new PEAR_Dependency2($c, $o, $p, $s); + return $z; + } + + function getInstalledBinary() + { + return isset($this->_packageInfo['#binarypackage']) ? $this->_packageInfo['#binarypackage'] : + false; + } + + /** + * Installation of source package has failed, attempt to download and install the + * binary version of this package. + * @param PEAR_Installer + * @return array|false + */ + function installBinary(&$installer) + { + if (!OS_WINDOWS) { + $a = false; + return $a; + } + if ($this->getPackageType() == 'extsrc' || $this->getPackageType() == 'zendextsrc') { + $releasetype = $this->getPackageType() . 'release'; + if (!is_array($installer->getInstallPackages())) { + $a = false; + return $a; + } + foreach ($installer->getInstallPackages() as $p) { + if ($p->isExtension($this->_packageInfo['providesextension'])) { + if ($p->getPackageType() != 'extsrc' && $p->getPackageType() != 'zendextsrc') { + $a = false; + return $a; // the user probably downloaded it separately + } + } + } + if (isset($this->_packageInfo[$releasetype]['binarypackage'])) { + $installer->log(0, 'Attempting to download binary version of extension "' . + $this->_packageInfo['providesextension'] . '"'); + $params = $this->_packageInfo[$releasetype]['binarypackage']; + if (!is_array($params) || !isset($params[0])) { + $params = array($params); + } + if (isset($this->_packageInfo['channel'])) { + foreach ($params as $i => $param) { + $params[$i] = array('channel' => $this->_packageInfo['channel'], + 'package' => $param, 'version' => $this->getVersion()); + } + } + $dl = &$this->getPEARDownloader($installer->ui, $installer->getOptions(), + $installer->config); + $verbose = $dl->config->get('verbose'); + $dl->config->set('verbose', -1); + foreach ($params as $param) { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $ret = $dl->download(array($param)); + PEAR::popErrorHandling(); + if (is_array($ret) && count($ret)) { + break; + } + } + $dl->config->set('verbose', $verbose); + if (is_array($ret)) { + if (count($ret) == 1) { + $pf = $ret[0]->getPackageFile(); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $installer->install($ret[0]); + PEAR::popErrorHandling(); + if (is_array($err)) { + $this->_packageInfo['#binarypackage'] = $ret[0]->getPackage(); + // "install" self, so all dependencies will work transparently + $this->_registry->addPackage2($this); + $installer->log(0, 'Download and install of binary extension "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pf->getChannel(), + 'package' => $pf->getPackage()), true) . '" successful'); + $a = array($ret[0], $err); + return $a; + } + $installer->log(0, 'Download and install of binary extension "' . + $this->_registry->parsedPackageNameToString( + array('channel' => $pf->getChannel(), + 'package' => $pf->getPackage()), true) . '" failed'); + } + } + } + } + $a = false; + return $a; + } + + /** + * @return string|false Extension name + */ + function getProvidesExtension() + { + if (in_array($this->getPackageType(), + array('extsrc', 'extbin', 'zendextsrc', 'zendextbin'))) { + if (isset($this->_packageInfo['providesextension'])) { + return $this->_packageInfo['providesextension']; + } + } + return false; + } + + /** + * @param string Extension name + * @return bool + */ + function isExtension($extension) + { + if (in_array($this->getPackageType(), + array('extsrc', 'extbin', 'zendextsrc', 'zendextbin'))) { + return $this->_packageInfo['providesextension'] == $extension; + } + return false; + } + + /** + * Tests whether every part of the package.xml 1.0 is represented in + * this package.xml 2.0 + * @param PEAR_PackageFile_v1 + * @return bool + */ + function isEquivalent($pf1) + { + if (!$pf1) { + return true; + } + if ($this->getPackageType() == 'bundle') { + return false; + } + $this->_stack->getErrors(true); + if (!$pf1->validate(PEAR_VALIDATE_NORMAL)) { + return false; + } + $pass = true; + if ($pf1->getPackage() != $this->getPackage()) { + $this->_differentPackage($pf1->getPackage()); + $pass = false; + } + if ($pf1->getVersion() != $this->getVersion()) { + $this->_differentVersion($pf1->getVersion()); + $pass = false; + } + if (trim($pf1->getSummary()) != $this->getSummary()) { + $this->_differentSummary($pf1->getSummary()); + $pass = false; + } + if (preg_replace('/\s+/', '', $pf1->getDescription()) != + preg_replace('/\s+/', '', $this->getDescription())) { + $this->_differentDescription($pf1->getDescription()); + $pass = false; + } + if ($pf1->getState() != $this->getState()) { + $this->_differentState($pf1->getState()); + $pass = false; + } + if (!strstr(preg_replace('/\s+/', '', $this->getNotes()), + preg_replace('/\s+/', '', $pf1->getNotes()))) { + $this->_differentNotes($pf1->getNotes()); + $pass = false; + } + $mymaintainers = $this->getMaintainers(); + $yourmaintainers = $pf1->getMaintainers(); + for ($i1 = 0; $i1 < count($yourmaintainers); $i1++) { + $reset = false; + for ($i2 = 0; $i2 < count($mymaintainers); $i2++) { + if ($mymaintainers[$i2]['handle'] == $yourmaintainers[$i1]['handle']) { + if ($mymaintainers[$i2]['role'] != $yourmaintainers[$i1]['role']) { + $this->_differentRole($mymaintainers[$i2]['handle'], + $yourmaintainers[$i1]['role'], $mymaintainers[$i2]['role']); + $pass = false; + } + if ($mymaintainers[$i2]['email'] != $yourmaintainers[$i1]['email']) { + $this->_differentEmail($mymaintainers[$i2]['handle'], + $yourmaintainers[$i1]['email'], $mymaintainers[$i2]['email']); + $pass = false; + } + if ($mymaintainers[$i2]['name'] != $yourmaintainers[$i1]['name']) { + $this->_differentName($mymaintainers[$i2]['handle'], + $yourmaintainers[$i1]['name'], $mymaintainers[$i2]['name']); + $pass = false; + } + unset($mymaintainers[$i2]); + $mymaintainers = array_values($mymaintainers); + unset($yourmaintainers[$i1]); + $yourmaintainers = array_values($yourmaintainers); + $reset = true; + break; + } + } + if ($reset) { + $i1 = -1; + } + } + $this->_unmatchedMaintainers($mymaintainers, $yourmaintainers); + $filelist = $this->getFilelist(); + foreach ($pf1->getFilelist() as $file => $atts) { + if (!isset($filelist[$file])) { + $this->_missingFile($file); + $pass = false; + } + } + return $pass; + } + + function _differentPackage($package) + { + $this->_stack->push(__FUNCTION__, 'error', array('package' => $package, + 'self' => $this->getPackage()), + 'package.xml 1.0 package "%package%" does not match "%self%"'); + } + + function _differentVersion($version) + { + $this->_stack->push(__FUNCTION__, 'error', array('version' => $version, + 'self' => $this->getVersion()), + 'package.xml 1.0 version "%version%" does not match "%self%"'); + } + + function _differentState($state) + { + $this->_stack->push(__FUNCTION__, 'error', array('state' => $state, + 'self' => $this->getState()), + 'package.xml 1.0 state "%state%" does not match "%self%"'); + } + + function _differentRole($handle, $role, $selfrole) + { + $this->_stack->push(__FUNCTION__, 'error', array('handle' => $handle, + 'role' => $role, 'self' => $selfrole), + 'package.xml 1.0 maintainer "%handle%" role "%role%" does not match "%self%"'); + } + + function _differentEmail($handle, $email, $selfemail) + { + $this->_stack->push(__FUNCTION__, 'error', array('handle' => $handle, + 'email' => $email, 'self' => $selfemail), + 'package.xml 1.0 maintainer "%handle%" email "%email%" does not match "%self%"'); + } + + function _differentName($handle, $name, $selfname) + { + $this->_stack->push(__FUNCTION__, 'error', array('handle' => $handle, + 'name' => $name, 'self' => $selfname), + 'package.xml 1.0 maintainer "%handle%" name "%name%" does not match "%self%"'); + } + + function _unmatchedMaintainers($my, $yours) + { + if ($my) { + array_walk($my, create_function('&$i, $k', '$i = $i["handle"];')); + $this->_stack->push(__FUNCTION__, 'error', array('handles' => $my), + 'package.xml 2.0 has unmatched extra maintainers "%handles%"'); + } + if ($yours) { + array_walk($yours, create_function('&$i, $k', '$i = $i["handle"];')); + $this->_stack->push(__FUNCTION__, 'error', array('handles' => $yours), + 'package.xml 1.0 has unmatched extra maintainers "%handles%"'); + } + } + + function _differentNotes($notes) + { + $truncnotes = strlen($notes) < 25 ? $notes : substr($notes, 0, 24) . '...'; + $truncmynotes = strlen($this->getNotes()) < 25 ? $this->getNotes() : + substr($this->getNotes(), 0, 24) . '...'; + $this->_stack->push(__FUNCTION__, 'error', array('notes' => $truncnotes, + 'self' => $truncmynotes), + 'package.xml 1.0 release notes "%notes%" do not match "%self%"'); + } + + function _differentSummary($summary) + { + $truncsummary = strlen($summary) < 25 ? $summary : substr($summary, 0, 24) . '...'; + $truncmysummary = strlen($this->getsummary()) < 25 ? $this->getSummary() : + substr($this->getsummary(), 0, 24) . '...'; + $this->_stack->push(__FUNCTION__, 'error', array('summary' => $truncsummary, + 'self' => $truncmysummary), + 'package.xml 1.0 summary "%summary%" does not match "%self%"'); + } + + function _differentDescription($description) + { + $truncdescription = trim(strlen($description) < 25 ? $description : substr($description, 0, 24) . '...'); + $truncmydescription = trim(strlen($this->getDescription()) < 25 ? $this->getDescription() : + substr($this->getdescription(), 0, 24) . '...'); + $this->_stack->push(__FUNCTION__, 'error', array('description' => $truncdescription, + 'self' => $truncmydescription), + 'package.xml 1.0 description "%description%" does not match "%self%"'); + } + + function _missingFile($file) + { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'package.xml 1.0 file "%file%" is not present in '); + } + + /** + * WARNING - do not use this function unless you know what you're doing + */ + function setRawState($state) + { + if (!isset($this->_packageInfo['stability'])) { + $this->_packageInfo['stability'] = array(); + } + $this->_packageInfo['stability']['release'] = $state; + } + + /** + * WARNING - do not use this function unless you know what you're doing + */ + function setRawCompatible($compatible) + { + $this->_packageInfo['compatible'] = $compatible; + } + + /** + * WARNING - do not use this function unless you know what you're doing + */ + function setRawPackage($package) + { + $this->_packageInfo['name'] = $package; + } + + /** + * WARNING - do not use this function unless you know what you're doing + */ + function setRawChannel($channel) + { + $this->_packageInfo['channel'] = $channel; + } + + function setRequestedGroup($group) + { + $this->_requestedGroup = $group; + } + + function getRequestedGroup() + { + if (isset($this->_requestedGroup)) { + return $this->_requestedGroup; + } + return false; + } + + /** + * For saving in the registry. + * + * Set the last version that was installed + * @param string + */ + function setLastInstalledVersion($version) + { + $this->_packageInfo['_lastversion'] = $version; + } + + /** + * @return string|false + */ + function getLastInstalledVersion() + { + if (isset($this->_packageInfo['_lastversion'])) { + return $this->_packageInfo['_lastversion']; + } + return false; + } + + /** + * Determines whether this package.xml has post-install scripts or not + * @return array|false + */ + function listPostinstallScripts() + { + $filelist = $this->getFilelist(); + $contents = $this->getContents(); + $contents = $contents['dir']['file']; + if (!is_array($contents) || !isset($contents[0])) { + $contents = array($contents); + } + $taskfiles = array(); + foreach ($contents as $file) { + $atts = $file['attribs']; + unset($file['attribs']); + if (count($file)) { + $taskfiles[$atts['name']] = $file; + } + } + $common = new PEAR_Common; + $common->debug = $this->_config->get('verbose'); + $this->_scripts = array(); + $ret = array(); + foreach ($taskfiles as $name => $tasks) { + if (!isset($filelist[$name])) { + // ignored files will not be in the filelist + continue; + } + $atts = $filelist[$name]; + foreach ($tasks as $tag => $raw) { + $task = $this->getTask($tag); + $task = &new $task($this->_config, $common, PEAR_TASK_INSTALL); + if ($task->isScript()) { + $ret[] = $filelist[$name]['installed_as']; + } + } + } + if (count($ret)) { + return $ret; + } + return false; + } + + /** + * Initialize post-install scripts for running + * + * This method can be used to detect post-install scripts, as the return value + * indicates whether any exist + * @return bool + */ + function initPostinstallScripts() + { + $filelist = $this->getFilelist(); + $contents = $this->getContents(); + $contents = $contents['dir']['file']; + if (!is_array($contents) || !isset($contents[0])) { + $contents = array($contents); + } + $taskfiles = array(); + foreach ($contents as $file) { + $atts = $file['attribs']; + unset($file['attribs']); + if (count($file)) { + $taskfiles[$atts['name']] = $file; + } + } + $common = new PEAR_Common; + $common->debug = $this->_config->get('verbose'); + $this->_scripts = array(); + foreach ($taskfiles as $name => $tasks) { + if (!isset($filelist[$name])) { + // file was not installed due to installconditions + continue; + } + $atts = $filelist[$name]; + foreach ($tasks as $tag => $raw) { + $taskname = $this->getTask($tag); + $task = &new $taskname($this->_config, $common, PEAR_TASK_INSTALL); + if (!$task->isScript()) { + continue; // scripts are only handled after installation + } + $lastversion = isset($this->_packageInfo['_lastversion']) ? + $this->_packageInfo['_lastversion'] : null; + $task->init($raw, $atts, $lastversion); + $res = $task->startSession($this, $atts['installed_as']); + if (!$res) { + continue; // skip this file + } + if (PEAR::isError($res)) { + return $res; + } + $assign = &$task; + $this->_scripts[] = &$assign; + } + } + if (count($this->_scripts)) { + return true; + } + return false; + } + + function runPostinstallScripts() + { + if ($this->initPostinstallScripts()) { + $ui = &PEAR_Frontend::singleton(); + if ($ui) { + $ui->runPostinstallScripts($this->_scripts, $this); + } + } + } + + + /** + * Convert a recursive set of and tags into a single tag with + * tags. + */ + function flattenFilelist() + { + if (isset($this->_packageInfo['bundle'])) { + return; + } + $filelist = array(); + if (isset($this->_packageInfo['contents']['dir']['dir'])) { + $this->_getFlattenedFilelist($filelist, $this->_packageInfo['contents']['dir']); + if (!isset($filelist[1])) { + $filelist = $filelist[0]; + } + $this->_packageInfo['contents']['dir']['file'] = $filelist; + unset($this->_packageInfo['contents']['dir']['dir']); + } else { + // else already flattened but check for baseinstalldir propagation + if (isset($this->_packageInfo['contents']['dir']['attribs']['baseinstalldir'])) { + if (isset($this->_packageInfo['contents']['dir']['file'][0])) { + foreach ($this->_packageInfo['contents']['dir']['file'] as $i => $file) { + if (isset($file['attribs']['baseinstalldir'])) { + continue; + } + $this->_packageInfo['contents']['dir']['file'][$i]['attribs']['baseinstalldir'] + = $this->_packageInfo['contents']['dir']['attribs']['baseinstalldir']; + } + } else { + if (!isset($this->_packageInfo['contents']['dir']['file']['attribs']['baseinstalldir'])) { + $this->_packageInfo['contents']['dir']['file']['attribs']['baseinstalldir'] + = $this->_packageInfo['contents']['dir']['attribs']['baseinstalldir']; + } + } + } + } + } + + /** + * @param array the final flattened file list + * @param array the current directory being processed + * @param string|false any recursively inherited baeinstalldir attribute + * @param string private recursion variable + * @return array + * @access protected + */ + function _getFlattenedFilelist(&$files, $dir, $baseinstall = false, $path = '') + { + if (isset($dir['attribs']) && isset($dir['attribs']['baseinstalldir'])) { + $baseinstall = $dir['attribs']['baseinstalldir']; + } + if (isset($dir['dir'])) { + if (!isset($dir['dir'][0])) { + $dir['dir'] = array($dir['dir']); + } + foreach ($dir['dir'] as $subdir) { + if (!isset($subdir['attribs']) || !isset($subdir['attribs']['name'])) { + $name = '*unknown*'; + } else { + $name = $subdir['attribs']['name']; + } + $newpath = empty($path) ? $name : + $path . '/' . $name; + $this->_getFlattenedFilelist($files, $subdir, + $baseinstall, $newpath); + } + } + if (isset($dir['file'])) { + if (!isset($dir['file'][0])) { + $dir['file'] = array($dir['file']); + } + foreach ($dir['file'] as $file) { + $attrs = $file['attribs']; + $name = $attrs['name']; + if ($baseinstall && !isset($attrs['baseinstalldir'])) { + $attrs['baseinstalldir'] = $baseinstall; + } + $attrs['name'] = empty($path) ? $name : $path . '/' . $name; + $attrs['name'] = preg_replace(array('!\\\\+!', '!/+!'), array('/', '/'), + $attrs['name']); + $file['attribs'] = $attrs; + $files[] = $file; + } + } + } + + function setConfig(&$config) + { + $this->_config = &$config; + $this->_registry = &$config->getRegistry(); + } + + function setLogger(&$logger) + { + if (!is_object($logger) || !method_exists($logger, 'log')) { + return PEAR::raiseError('Logger must be compatible with PEAR_Common::log'); + } + $this->_logger = &$logger; + } + + /** + * WARNING - do not use this function directly unless you know what you're doing + */ + function setDeps($deps) + { + $this->_packageInfo['dependencies'] = $deps; + } + + /** + * WARNING - do not use this function directly unless you know what you're doing + */ + function setCompatible($compat) + { + $this->_packageInfo['compatible'] = $compat; + } + + function setPackagefile($file, $archive = false) + { + $this->_packageFile = $file; + $this->_archiveFile = $archive ? $archive : $file; + } + + /** + * Wrapper to {@link PEAR_ErrorStack::getErrors()} + * @param boolean determines whether to purge the error stack after retrieving + * @return array + */ + function getValidationWarnings($purge = true) + { + return $this->_stack->getErrors($purge); + } + + function getPackageFile() + { + return $this->_packageFile; + } + + function getArchiveFile() + { + return $this->_archiveFile; + } + + + /** + * Directly set the array that defines this packagefile + * + * WARNING: no validation. This should only be performed by internal methods + * inside PEAR or by inputting an array saved from an existing PEAR_PackageFile_v2 + * @param array + */ + function fromArray($pinfo) + { + unset($pinfo['old']); + unset($pinfo['xsdversion']); + $this->_incomplete = false; + $this->_packageInfo = $pinfo; + } + + function isIncomplete() + { + return $this->_incomplete; + } + + /** + * @return array + */ + function toArray($forreg = false) + { + if (!$this->validate(PEAR_VALIDATE_NORMAL)) { + return false; + } + return $this->getArray($forreg); + } + + function getArray($forReg = false) + { + if ($forReg) { + $arr = $this->_packageInfo; + $arr['old'] = array(); + $arr['old']['version'] = $this->getVersion(); + $arr['old']['release_date'] = $this->getDate(); + $arr['old']['release_state'] = $this->getState(); + $arr['old']['release_license'] = $this->getLicense(); + $arr['old']['release_notes'] = $this->getNotes(); + $arr['old']['release_deps'] = $this->getDeps(); + $arr['old']['maintainers'] = $this->getMaintainers(); + $arr['xsdversion'] = '2.0'; + return $arr; + } else { + $info = $this->_packageInfo; + unset($info['dirtree']); + if (isset($info['_lastversion'])) { + unset($info['_lastversion']); + } + if (isset($info['#binarypackage'])) { + unset($info['#binarypackage']); + } + return $info; + } + } + + function packageInfo($field) + { + $arr = $this->getArray(true); + if ($field == 'state') { + return $arr['stability']['release']; + } + if ($field == 'api-version') { + return $arr['version']['api']; + } + if ($field == 'api-state') { + return $arr['stability']['api']; + } + if (isset($arr['old'][$field])) { + if (!is_string($arr['old'][$field])) { + return null; + } + return $arr['old'][$field]; + } + if (isset($arr[$field])) { + if (!is_string($arr[$field])) { + return null; + } + return $arr[$field]; + } + return null; + } + + function getName() + { + return $this->getPackage(); + } + + function getPackage() + { + if (isset($this->_packageInfo['name'])) { + return $this->_packageInfo['name']; + } + return false; + } + + function getChannel() + { + if (isset($this->_packageInfo['uri'])) { + return '__uri'; + } + if (isset($this->_packageInfo['channel'])) { + return strtolower($this->_packageInfo['channel']); + } + return false; + } + + function getUri() + { + if (isset($this->_packageInfo['uri'])) { + return $this->_packageInfo['uri']; + } + return false; + } + + function getExtends() + { + if (isset($this->_packageInfo['extends'])) { + return $this->_packageInfo['extends']; + } + return false; + } + + function getSummary() + { + if (isset($this->_packageInfo['summary'])) { + return $this->_packageInfo['summary']; + } + return false; + } + + function getDescription() + { + if (isset($this->_packageInfo['description'])) { + return $this->_packageInfo['description']; + } + return false; + } + + function getMaintainers($raw = false) + { + if (!isset($this->_packageInfo['lead'])) { + return false; + } + if ($raw) { + $ret = array('lead' => $this->_packageInfo['lead']); + (isset($this->_packageInfo['developer'])) ? + $ret['developer'] = $this->_packageInfo['developer'] :null; + (isset($this->_packageInfo['contributor'])) ? + $ret['contributor'] = $this->_packageInfo['contributor'] :null; + (isset($this->_packageInfo['helper'])) ? + $ret['helper'] = $this->_packageInfo['helper'] :null; + return $ret; + } else { + $ret = array(); + $leads = isset($this->_packageInfo['lead'][0]) ? $this->_packageInfo['lead'] : + array($this->_packageInfo['lead']); + foreach ($leads as $lead) { + $s = $lead; + $s['handle'] = $s['user']; + unset($s['user']); + $s['role'] = 'lead'; + $ret[] = $s; + } + if (isset($this->_packageInfo['developer'])) { + $leads = isset($this->_packageInfo['developer'][0]) ? + $this->_packageInfo['developer'] : + array($this->_packageInfo['developer']); + foreach ($leads as $maintainer) { + $s = $maintainer; + $s['handle'] = $s['user']; + unset($s['user']); + $s['role'] = 'developer'; + $ret[] = $s; + } + } + if (isset($this->_packageInfo['contributor'])) { + $leads = isset($this->_packageInfo['contributor'][0]) ? + $this->_packageInfo['contributor'] : + array($this->_packageInfo['contributor']); + foreach ($leads as $maintainer) { + $s = $maintainer; + $s['handle'] = $s['user']; + unset($s['user']); + $s['role'] = 'contributor'; + $ret[] = $s; + } + } + if (isset($this->_packageInfo['helper'])) { + $leads = isset($this->_packageInfo['helper'][0]) ? + $this->_packageInfo['helper'] : + array($this->_packageInfo['helper']); + foreach ($leads as $maintainer) { + $s = $maintainer; + $s['handle'] = $s['user']; + unset($s['user']); + $s['role'] = 'helper'; + $ret[] = $s; + } + } + return $ret; + } + return false; + } + + function getLeads() + { + if (isset($this->_packageInfo['lead'])) { + return $this->_packageInfo['lead']; + } + return false; + } + + function getDevelopers() + { + if (isset($this->_packageInfo['developer'])) { + return $this->_packageInfo['developer']; + } + return false; + } + + function getContributors() + { + if (isset($this->_packageInfo['contributor'])) { + return $this->_packageInfo['contributor']; + } + return false; + } + + function getHelpers() + { + if (isset($this->_packageInfo['helper'])) { + return $this->_packageInfo['helper']; + } + return false; + } + + function setDate($date) + { + if (!isset($this->_packageInfo['date'])) { + // ensure that the extends tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', + 'zendextbinrelease', 'bundle', 'changelog'), array(), 'date'); + } + $this->_packageInfo['date'] = $date; + $this->_isValid = 0; + } + + function setTime($time) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['time'])) { + // ensure that the time tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', + 'zendextbinrelease', 'bundle', 'changelog'), $time, 'time'); + } + $this->_packageInfo['time'] = $time; + } + + function getDate() + { + if (isset($this->_packageInfo['date'])) { + return $this->_packageInfo['date']; + } + return false; + } + + function getTime() + { + if (isset($this->_packageInfo['time'])) { + return $this->_packageInfo['time']; + } + return false; + } + + /** + * @param package|api version category to return + */ + function getVersion($key = 'release') + { + if (isset($this->_packageInfo['version'][$key])) { + return $this->_packageInfo['version'][$key]; + } + return false; + } + + function getStability() + { + if (isset($this->_packageInfo['stability'])) { + return $this->_packageInfo['stability']; + } + return false; + } + + function getState($key = 'release') + { + if (isset($this->_packageInfo['stability'][$key])) { + return $this->_packageInfo['stability'][$key]; + } + return false; + } + + function getLicense($raw = false) + { + if (isset($this->_packageInfo['license'])) { + if ($raw) { + return $this->_packageInfo['license']; + } + if (is_array($this->_packageInfo['license'])) { + return $this->_packageInfo['license']['_content']; + } else { + return $this->_packageInfo['license']; + } + } + return false; + } + + function getLicenseLocation() + { + if (!isset($this->_packageInfo['license']) || !is_array($this->_packageInfo['license'])) { + return false; + } + return $this->_packageInfo['license']['attribs']; + } + + function getNotes() + { + if (isset($this->_packageInfo['notes'])) { + return $this->_packageInfo['notes']; + } + return false; + } + + /** + * Return the tag contents, if any + * @return array|false + */ + function getUsesrole() + { + if (isset($this->_packageInfo['usesrole'])) { + return $this->_packageInfo['usesrole']; + } + return false; + } + + /** + * Return the tag contents, if any + * @return array|false + */ + function getUsestask() + { + if (isset($this->_packageInfo['usestask'])) { + return $this->_packageInfo['usestask']; + } + return false; + } + + /** + * This should only be used to retrieve filenames and install attributes + */ + function getFilelist($preserve = false) + { + if (isset($this->_packageInfo['filelist']) && !$preserve) { + return $this->_packageInfo['filelist']; + } + $this->flattenFilelist(); + if ($contents = $this->getContents()) { + $ret = array(); + if (!isset($contents['dir']['file'][0])) { + $contents['dir']['file'] = array($contents['dir']['file']); + } + foreach ($contents['dir']['file'] as $file) { + $name = $file['attribs']['name']; + if (!$preserve) { + $file = $file['attribs']; + } + $ret[$name] = $file; + } + if (!$preserve) { + $this->_packageInfo['filelist'] = $ret; + } + return $ret; + } + return false; + } + + /** + * Return configure options array, if any + * + * @return array|false + */ + function getConfigureOptions() + { + if ($this->getPackageType() != 'extsrc' && $this->getPackageType() != 'zendextsrc') { + return false; + } + $releases = $this->getReleases(); + if (isset($releases[0])) { + $releases = $releases[0]; + } + if (isset($releases['configureoption'])) { + if (!isset($releases['configureoption'][0])) { + $releases['configureoption'] = array($releases['configureoption']); + } + for ($i = 0; $i < count($releases['configureoption']); $i++) { + $releases['configureoption'][$i] = $releases['configureoption'][$i]['attribs']; + } + return $releases['configureoption']; + } + return false; + } + + /** + * This is only used at install-time, after all serialization + * is over. + */ + function resetFilelist() + { + $this->_packageInfo['filelist'] = array(); + } + + /** + * Retrieve a list of files that should be installed on this computer + * @return array + */ + function getInstallationFilelist($forfilecheck = false) + { + $contents = $this->getFilelist(true); + if (isset($contents['dir']['attribs']['baseinstalldir'])) { + $base = $contents['dir']['attribs']['baseinstalldir']; + } + if (isset($this->_packageInfo['bundle'])) { + return PEAR::raiseError( + 'Exception: bundles should be handled in download code only'); + } + $release = $this->getReleases(); + if ($release) { + if (!isset($release[0])) { + if (!isset($release['installconditions']) && !isset($release['filelist'])) { + if ($forfilecheck) { + return $this->getFilelist(); + } + return $contents; + } + $release = array($release); + } + $depchecker = &$this->getPEARDependency2($this->_config, array(), + array('channel' => $this->getChannel(), 'package' => $this->getPackage()), + PEAR_VALIDATE_INSTALLING); + foreach ($release as $instance) { + if (isset($instance['installconditions'])) { + $installconditions = $instance['installconditions']; + if (is_array($installconditions)) { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + foreach ($installconditions as $type => $conditions) { + if (!isset($conditions[0])) { + $conditions = array($conditions); + } + foreach ($conditions as $condition) { + $ret = $depchecker->{"validate{$type}Dependency"}($condition); + if (PEAR::isError($ret)) { + PEAR::popErrorHandling(); + continue 3; // skip this release + } + } + } + PEAR::popErrorHandling(); + } + } + // this is the release to use + if (isset($instance['filelist'])) { + // ignore files + if (isset($instance['filelist']['ignore'])) { + $ignore = isset($instance['filelist']['ignore'][0]) ? + $instance['filelist']['ignore'] : + array($instance['filelist']['ignore']); + foreach ($ignore as $ig) { + unset ($contents[$ig['attribs']['name']]); + } + } + // install files as this name + if (isset($instance['filelist']['install'])) { + $installas = isset($instance['filelist']['install'][0]) ? + $instance['filelist']['install'] : + array($instance['filelist']['install']); + foreach ($installas as $as) { + $contents[$as['attribs']['name']]['attribs']['install-as'] = + $as['attribs']['as']; + } + } + } + if ($forfilecheck) { + foreach ($contents as $file => $attrs) { + $contents[$file] = $attrs['attribs']; + } + } + return $contents; + } + } else { // simple release - no installconditions or install-as + if ($forfilecheck) { + return $this->getFilelist(); + } + return $contents; + } + // no releases matched + return PEAR::raiseError('No releases in package.xml matched the existing operating ' . + 'system, extensions installed, or architecture, cannot install'); + } + + /** + * This is only used at install-time, after all serialization + * is over. + * @param string file name + * @param string installed path + */ + function setInstalledAs($file, $path) + { + if ($path) { + return $this->_packageInfo['filelist'][$file]['installed_as'] = $path; + } + unset($this->_packageInfo['filelist'][$file]['installed_as']); + } + + function getInstalledLocation($file) + { + if (isset($this->_packageInfo['filelist'][$file]['installed_as'])) { + return $this->_packageInfo['filelist'][$file]['installed_as']; + } + return false; + } + + /** + * This is only used at install-time, after all serialization + * is over. + */ + function installedFile($file, $atts) + { + if (isset($this->_packageInfo['filelist'][$file])) { + $this->_packageInfo['filelist'][$file] = + array_merge($this->_packageInfo['filelist'][$file], $atts['attribs']); + } else { + $this->_packageInfo['filelist'][$file] = $atts['attribs']; + } + } + + /** + * Retrieve the contents tag + */ + function getContents() + { + if (isset($this->_packageInfo['contents'])) { + return $this->_packageInfo['contents']; + } + return false; + } + + /** + * @param string full path to file + * @param string attribute name + * @param string attribute value + * @param int risky but fast - use this to choose a file based on its position in the list + * of files. Index is zero-based like PHP arrays. + * @return bool success of operation + */ + function setFileAttribute($filename, $attr, $value, $index = false) + { + $this->_isValid = 0; + if (in_array($attr, array('role', 'name', 'baseinstalldir'))) { + $this->_filesValid = false; + } + if ($index !== false && + isset($this->_packageInfo['contents']['dir']['file'][$index]['attribs'])) { + $this->_packageInfo['contents']['dir']['file'][$index]['attribs'][$attr] = $value; + return true; + } + if (!isset($this->_packageInfo['contents']['dir']['file'])) { + return false; + } + $files = $this->_packageInfo['contents']['dir']['file']; + if (!isset($files[0])) { + $files = array($files); + $ind = false; + } else { + $ind = true; + } + foreach ($files as $i => $file) { + if (isset($file['attribs'])) { + if ($file['attribs']['name'] == $filename) { + if ($ind) { + $this->_packageInfo['contents']['dir']['file'][$i]['attribs'][$attr] = $value; + } else { + $this->_packageInfo['contents']['dir']['file']['attribs'][$attr] = $value; + } + return true; + } + } + } + return false; + } + + function setDirtree($path) + { + if (!isset($this->_packageInfo['dirtree'])) { + $this->_packageInfo['dirtree'] = array(); + } + $this->_packageInfo['dirtree'][$path] = true; + } + + function getDirtree() + { + if (isset($this->_packageInfo['dirtree']) && count($this->_packageInfo['dirtree'])) { + return $this->_packageInfo['dirtree']; + } + return false; + } + + function resetDirtree() + { + unset($this->_packageInfo['dirtree']); + } + + /** + * Determines whether this package claims it is compatible with the version of + * the package that has a recommended version dependency + * @param PEAR_PackageFile_v2|PEAR_PackageFile_v1|PEAR_Downloader_Package + * @return boolean + */ + function isCompatible($pf) + { + if (!isset($this->_packageInfo['compatible'])) { + return false; + } + if (!isset($this->_packageInfo['channel'])) { + return false; + } + $me = $pf->getVersion(); + $compatible = $this->_packageInfo['compatible']; + if (!isset($compatible[0])) { + $compatible = array($compatible); + } + $found = false; + foreach ($compatible as $info) { + if (strtolower($info['name']) == strtolower($pf->getPackage())) { + if (strtolower($info['channel']) == strtolower($pf->getChannel())) { + $found = true; + break; + } + } + } + if (!$found) { + return false; + } + if (isset($info['exclude'])) { + if (!isset($info['exclude'][0])) { + $info['exclude'] = array($info['exclude']); + } + foreach ($info['exclude'] as $exclude) { + if (version_compare($me, $exclude, '==')) { + return false; + } + } + } + if (version_compare($me, $info['min'], '>=') && version_compare($me, $info['max'], '<=')) { + return true; + } + return false; + } + + /** + * @return array|false + */ + function getCompatible() + { + if (isset($this->_packageInfo['compatible'])) { + return $this->_packageInfo['compatible']; + } + return false; + } + + function getDependencies() + { + if (isset($this->_packageInfo['dependencies'])) { + return $this->_packageInfo['dependencies']; + } + return false; + } + + function isSubpackageOf($p) + { + return $p->isSubpackage($this); + } + + /** + * Determines whether the passed in package is a subpackage of this package. + * + * No version checking is done, only name verification. + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @return bool + */ + function isSubpackage($p) + { + $sub = array(); + if (isset($this->_packageInfo['dependencies']['required']['subpackage'])) { + $sub = $this->_packageInfo['dependencies']['required']['subpackage']; + if (!isset($sub[0])) { + $sub = array($sub); + } + } + if (isset($this->_packageInfo['dependencies']['optional']['subpackage'])) { + $sub1 = $this->_packageInfo['dependencies']['optional']['subpackage']; + if (!isset($sub1[0])) { + $sub1 = array($sub1); + } + $sub = array_merge($sub, $sub1); + } + if (isset($this->_packageInfo['dependencies']['group'])) { + $group = $this->_packageInfo['dependencies']['group']; + if (!isset($group[0])) { + $group = array($group); + } + foreach ($group as $deps) { + if (isset($deps['subpackage'])) { + $sub2 = $deps['subpackage']; + if (!isset($sub2[0])) { + $sub2 = array($sub2); + } + $sub = array_merge($sub, $sub2); + } + } + } + foreach ($sub as $dep) { + if (strtolower($dep['name']) == strtolower($p->getPackage())) { + if (isset($dep['channel'])) { + if (strtolower($dep['channel']) == strtolower($p->getChannel())) { + return true; + } + } else { + if ($dep['uri'] == $p->getURI()) { + return true; + } + } + } + } + return false; + } + + function dependsOn($package, $channel) + { + if (!($deps = $this->getDependencies())) { + return false; + } + foreach (array('package', 'subpackage') as $type) { + foreach (array('required', 'optional') as $needed) { + if (isset($deps[$needed][$type])) { + if (!isset($deps[$needed][$type][0])) { + $deps[$needed][$type] = array($deps[$needed][$type]); + } + foreach ($deps[$needed][$type] as $dep) { + $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri'; + if (strtolower($dep['name']) == strtolower($package) && + $depchannel == $channel) { + return true; + } + } + } + } + if (isset($deps['group'])) { + if (!isset($deps['group'][0])) { + $dep['group'] = array($deps['group']); + } + foreach ($deps['group'] as $group) { + if (isset($group[$type])) { + if (!is_array($group[$type])) { + $group[$type] = array($group[$type]); + } + foreach ($group[$type] as $dep) { + $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri'; + if (strtolower($dep['name']) == strtolower($package) && + $depchannel == $channel) { + return true; + } + } + } + } + } + } + return false; + } + + /** + * Get the contents of a dependency group + * @param string + * @return array|false + */ + function getDependencyGroup($name) + { + $name = strtolower($name); + if (!isset($this->_packageInfo['dependencies']['group'])) { + return false; + } + $groups = $this->_packageInfo['dependencies']['group']; + if (!isset($groups[0])) { + $groups = array($groups); + } + foreach ($groups as $group) { + if (strtolower($group['attribs']['name']) == $name) { + return $group; + } + } + return false; + } + + /** + * Retrieve a partial package.xml 1.0 representation of dependencies + * + * a very limited representation of dependencies is returned by this method. + * The tag for excluding certain versions of a dependency is + * completely ignored. In addition, dependency groups are ignored, with the + * assumption that all dependencies in dependency groups are also listed in + * the optional group that work with all dependency groups + * @param boolean return package.xml 2.0 tag + * @return array|false + */ + function getDeps($raw = false, $nopearinstaller = false) + { + if (isset($this->_packageInfo['dependencies'])) { + if ($raw) { + return $this->_packageInfo['dependencies']; + } + $ret = array(); + $map = array( + 'php' => 'php', + 'package' => 'pkg', + 'subpackage' => 'pkg', + 'extension' => 'ext', + 'os' => 'os', + 'pearinstaller' => 'pkg', + ); + foreach (array('required', 'optional') as $type) { + $optional = ($type == 'optional') ? 'yes' : 'no'; + if (!isset($this->_packageInfo['dependencies'][$type])) { + continue; + } + foreach ($this->_packageInfo['dependencies'][$type] as $dtype => $deps) { + if ($dtype == 'pearinstaller' && $nopearinstaller) { + continue; + } + if (!isset($deps[0])) { + $deps = array($deps); + } + foreach ($deps as $dep) { + if (!isset($map[$dtype])) { + // no support for arch type + continue; + } + if ($dtype == 'pearinstaller') { + $dep['name'] = 'PEAR'; + $dep['channel'] = 'pear.php.net'; + } + $s = array('type' => $map[$dtype]); + if (isset($dep['channel'])) { + $s['channel'] = $dep['channel']; + } + if (isset($dep['uri'])) { + $s['uri'] = $dep['uri']; + } + if (isset($dep['name'])) { + $s['name'] = $dep['name']; + } + if (isset($dep['conflicts'])) { + $s['rel'] = 'not'; + } else { + if (!isset($dep['min']) && + !isset($dep['max'])) { + $s['rel'] = 'has'; + $s['optional'] = $optional; + } elseif (isset($dep['min']) && + isset($dep['max'])) { + $s['rel'] = 'ge'; + $s1 = $s; + $s1['rel'] = 'le'; + $s['version'] = $dep['min']; + $s1['version'] = $dep['max']; + if (isset($dep['channel'])) { + $s1['channel'] = $dep['channel']; + } + if ($dtype != 'php') { + $s['name'] = $dep['name']; + $s1['name'] = $dep['name']; + } + $s['optional'] = $optional; + $s1['optional'] = $optional; + $ret[] = $s1; + } elseif (isset($dep['min'])) { + if (isset($dep['exclude']) && + $dep['exclude'] == $dep['min']) { + $s['rel'] = 'gt'; + } else { + $s['rel'] = 'ge'; + } + $s['version'] = $dep['min']; + $s['optional'] = $optional; + if ($dtype != 'php') { + $s['name'] = $dep['name']; + } + } elseif (isset($dep['max'])) { + if (isset($dep['exclude']) && + $dep['exclude'] == $dep['max']) { + $s['rel'] = 'lt'; + } else { + $s['rel'] = 'le'; + } + $s['version'] = $dep['max']; + $s['optional'] = $optional; + if ($dtype != 'php') { + $s['name'] = $dep['name']; + } + } + } + $ret[] = $s; + } + } + } + if (count($ret)) { + return $ret; + } + } + return false; + } + + /** + * @return php|extsrc|extbin|zendextsrc|zendextbin|bundle|false + */ + function getPackageType() + { + if (isset($this->_packageInfo['phprelease'])) { + return 'php'; + } + if (isset($this->_packageInfo['extsrcrelease'])) { + return 'extsrc'; + } + if (isset($this->_packageInfo['extbinrelease'])) { + return 'extbin'; + } + if (isset($this->_packageInfo['zendextsrcrelease'])) { + return 'zendextsrc'; + } + if (isset($this->_packageInfo['zendextbinrelease'])) { + return 'zendextbin'; + } + if (isset($this->_packageInfo['bundle'])) { + return 'bundle'; + } + return false; + } + + /** + * @return array|false + */ + function getReleases() + { + $type = $this->getPackageType(); + if ($type != 'bundle') { + $type .= 'release'; + } + if ($this->getPackageType() && isset($this->_packageInfo[$type])) { + return $this->_packageInfo[$type]; + } + return false; + } + + /** + * @return array + */ + function getChangelog() + { + if (isset($this->_packageInfo['changelog'])) { + return $this->_packageInfo['changelog']; + } + return false; + } + + function hasDeps() + { + return isset($this->_packageInfo['dependencies']); + } + + function getPackagexmlVersion() + { + if (isset($this->_packageInfo['zendextsrcrelease'])) { + return '2.1'; + } + if (isset($this->_packageInfo['zendextbinrelease'])) { + return '2.1'; + } + return '2.0'; + } + + /** + * @return array|false + */ + function getSourcePackage() + { + if (isset($this->_packageInfo['extbinrelease']) || + isset($this->_packageInfo['zendextbinrelease'])) { + return array('channel' => $this->_packageInfo['srcchannel'], + 'package' => $this->_packageInfo['srcpackage']); + } + return false; + } + + function getBundledPackages() + { + if (isset($this->_packageInfo['bundle'])) { + return $this->_packageInfo['contents']['bundledpackage']; + } + return false; + } + + function getLastModified() + { + if (isset($this->_packageInfo['_lastmodified'])) { + return $this->_packageInfo['_lastmodified']; + } + return false; + } + + /** + * Get the contents of a file listed within the package.xml + * @param string + * @return string + */ + function getFileContents($file) + { + if ($this->_archiveFile == $this->_packageFile) { // unpacked + $dir = dirname($this->_packageFile); + $file = $dir . DIRECTORY_SEPARATOR . $file; + $file = str_replace(array('/', '\\'), + array(DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $file); + if (file_exists($file) && is_readable($file)) { + return implode('', file($file)); + } + } else { // tgz + $tar = &new Archive_Tar($this->_archiveFile); + $tar->pushErrorHandling(PEAR_ERROR_RETURN); + if ($file != 'package.xml' && $file != 'package2.xml') { + $file = $this->getPackage() . '-' . $this->getVersion() . '/' . $file; + } + $file = $tar->extractInString($file); + $tar->popErrorHandling(); + if (PEAR::isError($file)) { + return PEAR::raiseError("Cannot locate file '$file' in archive"); + } + return $file; + } + } + + function &getRW() + { + if (!class_exists('PEAR_PackageFile_v2_rw')) { + require_once 'PEAR/PackageFile/v2/rw.php'; + } + $a = new PEAR_PackageFile_v2_rw; + foreach (get_object_vars($this) as $name => $unused) { + if (!isset($this->$name)) { + continue; + } + if ($name == '_config' || $name == '_logger'|| $name == '_registry' || + $name == '_stack') { + $a->$name = &$this->$name; + } else { + $a->$name = $this->$name; + } + } + return $a; + } + + function &getDefaultGenerator() + { + if (!class_exists('PEAR_PackageFile_Generator_v2')) { + require_once 'PEAR/PackageFile/Generator/v2.php'; + } + $a = &new PEAR_PackageFile_Generator_v2($this); + return $a; + } + + function analyzeSourceCode($file, $string = false) + { + if (!isset($this->_v2Validator) || + !is_a($this->_v2Validator, 'PEAR_PackageFile_v2_Validator')) { + if (!class_exists('PEAR_PackageFile_v2_Validator')) { + require_once 'PEAR/PackageFile/v2/Validator.php'; + } + $this->_v2Validator = new PEAR_PackageFile_v2_Validator; + } + return $this->_v2Validator->analyzeSourceCode($file, $string); + } + + function validate($state = PEAR_VALIDATE_NORMAL) + { + if (!isset($this->_packageInfo) || !is_array($this->_packageInfo)) { + return false; + } + if (!isset($this->_v2Validator) || + !is_a($this->_v2Validator, 'PEAR_PackageFile_v2_Validator')) { + if (!class_exists('PEAR_PackageFile_v2_Validator')) { + require_once 'PEAR/PackageFile/v2/Validator.php'; + } + $this->_v2Validator = new PEAR_PackageFile_v2_Validator; + } + if (isset($this->_packageInfo['xsdversion'])) { + unset($this->_packageInfo['xsdversion']); + } + return $this->_v2Validator->validate($this, $state); + } + + function getTasksNs() + { + if (!isset($this->_tasksNs)) { + if (isset($this->_packageInfo['attribs'])) { + foreach ($this->_packageInfo['attribs'] as $name => $value) { + if ($value == 'http://pear.php.net/dtd/tasks-1.0') { + $this->_tasksNs = str_replace('xmlns:', '', $name); + break; + } + } + } + } + return $this->_tasksNs; + } + + /** + * Determine whether a task name is a valid task. Custom tasks may be defined + * using subdirectories by putting a "-" in the name, as in + * + * Note that this method will auto-load the task class file and test for the existence + * of the name with "-" replaced by "_" as in PEAR/Task/mycustom/task.php makes class + * PEAR_Task_mycustom_task + * @param string + * @return boolean + */ + function getTask($task) + { + $this->getTasksNs(); + // transform all '-' to '/' and 'tasks:' to '' so tasks:replace becomes replace + $task = str_replace(array($this->_tasksNs . ':', '-'), array('', ' '), $task); + $taskfile = str_replace(' ', '/', ucwords($task)); + $task = str_replace(array(' ', '/'), '_', ucwords($task)); + if (class_exists("PEAR_Task_$task")) { + return "PEAR_Task_$task"; + } + $fp = @fopen("PEAR/Task/$taskfile.php", 'r', true); + if ($fp) { + fclose($fp); + require_once "PEAR/Task/$taskfile.php"; + return "PEAR_Task_$task"; + } + return false; + } + + /** + * Key-friendly array_splice + * @param tagname to splice a value in before + * @param mixed the value to splice in + * @param string the new tag name + */ + function _ksplice($array, $key, $value, $newkey) + { + $offset = array_search($key, array_keys($array)); + $after = array_slice($array, $offset); + $before = array_slice($array, 0, $offset); + $before[$newkey] = $value; + return array_merge($before, $after); + } + + /** + * @param array a list of possible keys, in the order they may occur + * @param mixed contents of the new package.xml tag + * @param string tag name + * @access private + */ + function _insertBefore($array, $keys, $contents, $newkey) + { + foreach ($keys as $key) { + if (isset($array[$key])) { + return $array = $this->_ksplice($array, $key, $contents, $newkey); + } + } + $array[$newkey] = $contents; + return $array; + } + + /** + * @param subsection of {@link $_packageInfo} + * @param array|string tag contents + * @param array format: + *
+     * array(
+     *   tagname => array(list of tag names that follow this one),
+     *   childtagname => array(list of child tag names that follow this one),
+     * )
+     * 
+ * + * This allows construction of nested tags + * @access private + */ + function _mergeTag($manip, $contents, $order) + { + if (count($order)) { + foreach ($order as $tag => $curorder) { + if (!isset($manip[$tag])) { + // ensure that the tag is set up + $manip = $this->_insertBefore($manip, $curorder, array(), $tag); + } + if (count($order) > 1) { + $manip[$tag] = $this->_mergeTag($manip[$tag], $contents, array_slice($order, 1)); + return $manip; + } + } + } else { + return $manip; + } + if (is_array($manip[$tag]) && !empty($manip[$tag]) && isset($manip[$tag][0])) { + $manip[$tag][] = $contents; + } else { + if (!count($manip[$tag])) { + $manip[$tag] = $contents; + } else { + $manip[$tag] = array($manip[$tag]); + $manip[$tag][] = $contents; + } + } + return $manip; + } +} +?> diff --git a/PEAR/PackageFile/v2/Validator.php b/PEAR/PackageFile/v2/Validator.php new file mode 100644 index 0000000..a0de15a --- /dev/null +++ b/PEAR/PackageFile/v2/Validator.php @@ -0,0 +1,2101 @@ + | +// | | +// +----------------------------------------------------------------------+ +// +// $Id: Validator.php,v 1.102 2007/06/10 04:16:51 cellog Exp $ +/** + * Private validation class used by PEAR_PackageFile_v2 - do not use directly, its + * sole purpose is to split up the PEAR/PackageFile/v2.php file to make it smaller + * @author Greg Beaver + * @access private + */ +class PEAR_PackageFile_v2_Validator +{ + /** + * @var array + */ + var $_packageInfo; + /** + * @var PEAR_PackageFile_v2 + */ + var $_pf; + /** + * @var PEAR_ErrorStack + */ + var $_stack; + /** + * @var int + */ + var $_isValid = 0; + /** + * @var int + */ + var $_filesValid = 0; + /** + * @var int + */ + var $_curState = 0; + /** + * @param PEAR_PackageFile_v2 + * @param int + */ + function validate(&$pf, $state = PEAR_VALIDATE_NORMAL) + { + $this->_pf = &$pf; + $this->_curState = $state; + $this->_packageInfo = $this->_pf->getArray(); + $this->_isValid = $this->_pf->_isValid; + $this->_filesValid = $this->_pf->_filesValid; + $this->_stack = &$pf->_stack; + $this->_stack->getErrors(true); + if (($this->_isValid & $state) == $state) { + return true; + } + if (!isset($this->_packageInfo) || !is_array($this->_packageInfo)) { + return false; + } + if (!isset($this->_packageInfo['attribs']['version']) || + ($this->_packageInfo['attribs']['version'] != '2.0' && + $this->_packageInfo['attribs']['version'] != '2.1')) { + $this->_noPackageVersion(); + } + $structure = + array( + 'name', + 'channel|uri', + '*extends', // can't be multiple, but this works fine + 'summary', + 'description', + '+lead', // these all need content checks + '*developer', + '*contributor', + '*helper', + 'date', + '*time', + 'version', + 'stability', + 'license->?uri->?filesource', + 'notes', + 'contents', //special validation needed + '*compatible', + 'dependencies', //special validation needed + '*usesrole', + '*usestask', // reserve these for 1.4.0a1 to implement + // this will allow a package.xml to gracefully say it + // needs a certain package installed in order to implement a role or task + '*providesextension', + '*srcpackage|*srcuri', + '+phprelease|+extsrcrelease|+extbinrelease|' . + '+zendextsrcrelease|+zendextbinrelease|bundle', //special validation needed + '*changelog', + ); + $test = $this->_packageInfo; + if (isset($test['dependencies']) && + isset($test['dependencies']['required']) && + isset($test['dependencies']['required']['pearinstaller']) && + isset($test['dependencies']['required']['pearinstaller']['min']) && + version_compare('1.6.1', + $test['dependencies']['required']['pearinstaller']['min'], '<')) { + $this->_pearVersionTooLow($test['dependencies']['required']['pearinstaller']['min']); + return false; + } + // ignore post-installation array fields + if (array_key_exists('filelist', $test)) { + unset($test['filelist']); + } + if (array_key_exists('_lastmodified', $test)) { + unset($test['_lastmodified']); + } + if (array_key_exists('#binarypackage', $test)) { + unset($test['#binarypackage']); + } + if (array_key_exists('old', $test)) { + unset($test['old']); + } + if (array_key_exists('_lastversion', $test)) { + unset($test['_lastversion']); + } + if (!$this->_stupidSchemaValidate($structure, + $test, '')) { + return false; + } + if (empty($this->_packageInfo['name'])) { + $this->_tagCannotBeEmpty('name'); + } + if (isset($this->_packageInfo['uri'])) { + $test = 'uri'; + } else { + $test = 'channel'; + } + if (empty($this->_packageInfo[$test])) { + $this->_tagCannotBeEmpty($test); + } + if (is_array($this->_packageInfo['license']) && + (!isset($this->_packageInfo['license']['_content']) || + empty($this->_packageInfo['license']['_content']))) { + $this->_tagCannotBeEmpty('license'); + } elseif (empty($this->_packageInfo['license'])) { + $this->_tagCannotBeEmpty('license'); + } + if (empty($this->_packageInfo['summary'])) { + $this->_tagCannotBeEmpty('summary'); + } + if (empty($this->_packageInfo['description'])) { + $this->_tagCannotBeEmpty('description'); + } + if (empty($this->_packageInfo['date'])) { + $this->_tagCannotBeEmpty('date'); + } + if (empty($this->_packageInfo['notes'])) { + $this->_tagCannotBeEmpty('notes'); + } + if (isset($this->_packageInfo['time']) && empty($this->_packageInfo['time'])) { + $this->_tagCannotBeEmpty('time'); + } + if (isset($this->_packageInfo['dependencies'])) { + $this->_validateDependencies(); + } + if (isset($this->_packageInfo['compatible'])) { + $this->_validateCompatible(); + } + if (!isset($this->_packageInfo['bundle'])) { + if (empty($this->_packageInfo['contents'])) { + $this->_tagCannotBeEmpty('contents'); + } + if (!isset($this->_packageInfo['contents']['dir'])) { + $this->_filelistMustContainDir('contents'); + return false; + } + if (isset($this->_packageInfo['contents']['file'])) { + $this->_filelistCannotContainFile('contents'); + return false; + } + } + $this->_validateMaintainers(); + $this->_validateStabilityVersion(); + $fail = false; + if (array_key_exists('usesrole', $this->_packageInfo)) { + $roles = $this->_packageInfo['usesrole']; + if (!is_array($roles) || !isset($roles[0])) { + $roles = array($roles); + } + foreach ($roles as $role) { + if (!isset($role['role'])) { + $this->_usesroletaskMustHaveRoleTask('usesrole', 'role'); + $fail = true; + } else { + if (!isset($role['channel'])) { + if (!isset($role['uri'])) { + $this->_usesroletaskMustHaveChannelOrUri($role['role'], 'usesrole'); + $fail = true; + } + } elseif (!isset($role['package'])) { + $this->_usesroletaskMustHavePackage($role['role'], 'usesrole'); + $fail = true; + } + } + } + } + if (array_key_exists('usestask', $this->_packageInfo)) { + $roles = $this->_packageInfo['usestask']; + if (!is_array($roles) || !isset($roles[0])) { + $roles = array($roles); + } + foreach ($roles as $role) { + if (!isset($role['task'])) { + $this->_usesroletaskMustHaveRoleTask('usestask', 'task'); + $fail = true; + } else { + if (!isset($role['channel'])) { + if (!isset($role['uri'])) { + $this->_usesroletaskMustHaveChannelOrUri($role['task'], 'usestask'); + $fail = true; + } + } elseif (!isset($role['package'])) { + $this->_usesroletaskMustHavePackage($role['task'], 'usestask'); + $fail = true; + } + } + } + } + if ($fail) { + return false; + } + $list = $this->_packageInfo['contents']; + if (isset($list['dir']) && is_array($list['dir']) && isset($list['dir'][0])) { + $this->_multipleToplevelDirNotAllowed(); + return $this->_isValid = 0; + } + $this->_validateFilelist(); + $this->_validateRelease(); + if (!$this->_stack->hasErrors()) { + $chan = $this->_pf->_registry->getChannel($this->_pf->getChannel(), true); + if (PEAR::isError($chan)) { + $this->_unknownChannel($this->_pf->getChannel()); + } else { + $valpack = $chan->getValidationPackage(); + // for channel validator packages, always use the default PEAR validator. + // otherwise, they can't be installed or packaged + $validator = $chan->getValidationObject($this->_pf->getPackage()); + if (!$validator) { + $this->_stack->push(__FUNCTION__, 'error', + array_merge( + array('channel' => $chan->getName(), + 'package' => $this->_pf->getPackage()), + $valpack + ), + 'package "%channel%/%package%" cannot be properly validated without ' . + 'validation package "%channel%/%name%-%version%"'); + return $this->_isValid = 0; + } + $validator->setPackageFile($this->_pf); + $validator->validate($state); + $failures = $validator->getFailures(); + foreach ($failures['errors'] as $error) { + $this->_stack->push(__FUNCTION__, 'error', $error, + 'Channel validator error: field "%field%" - %reason%'); + } + foreach ($failures['warnings'] as $warning) { + $this->_stack->push(__FUNCTION__, 'warning', $warning, + 'Channel validator warning: field "%field%" - %reason%'); + } + } + } + $this->_pf->_isValid = $this->_isValid = !$this->_stack->hasErrors('error'); + if ($this->_isValid && $state == PEAR_VALIDATE_PACKAGING && !$this->_filesValid) { + if ($this->_pf->getPackageType() == 'bundle') { + if ($this->_analyzeBundledPackages()) { + $this->_filesValid = $this->_pf->_filesValid = true; + } else { + $this->_pf->_isValid = $this->_isValid = 0; + } + } else { + if (!$this->_analyzePhpFiles()) { + $this->_pf->_isValid = $this->_isValid = 0; + } else { + $this->_filesValid = $this->_pf->_filesValid = true; + } + } + } + if ($this->_isValid) { + return $this->_pf->_isValid = $this->_isValid = $state; + } + return $this->_pf->_isValid = $this->_isValid = 0; + } + + function _stupidSchemaValidate($structure, $xml, $root) + { + if (!is_array($xml)) { + $xml = array(); + } + $keys = array_keys($xml); + reset($keys); + $key = current($keys); + while ($key == 'attribs' || $key == '_contents') { + $key = next($keys); + } + $unfoundtags = $optionaltags = array(); + $ret = true; + $mismatch = false; + foreach ($structure as $struc) { + if ($key) { + $tag = $xml[$key]; + } + $test = $this->_processStructure($struc); + if (isset($test['choices'])) { + $loose = true; + foreach ($test['choices'] as $choice) { + if ($key == $choice['tag']) { + $key = next($keys); + while ($key == 'attribs' || $key == '_contents') { + $key = next($keys); + } + $unfoundtags = $optionaltags = array(); + $mismatch = false; + if ($key && $key != $choice['tag'] && isset($choice['multiple'])) { + $unfoundtags[] = $choice['tag']; + $optionaltags[] = $choice['tag']; + if ($key) { + $mismatch = true; + } + } + $ret &= $this->_processAttribs($choice, $tag, $root); + continue 2; + } else { + $unfoundtags[] = $choice['tag']; + $mismatch = true; + } + if (!isset($choice['multiple']) || $choice['multiple'] != '*') { + $loose = false; + } else { + $optionaltags[] = $choice['tag']; + } + } + if (!$loose) { + $this->_invalidTagOrder($unfoundtags, $key, $root); + return false; + } + } else { + if ($key != $test['tag']) { + if (isset($test['multiple']) && $test['multiple'] != '*') { + $unfoundtags[] = $test['tag']; + $this->_invalidTagOrder($unfoundtags, $key, $root); + return false; + } else { + if ($key) { + $mismatch = true; + } + $unfoundtags[] = $test['tag']; + $optionaltags[] = $test['tag']; + } + if (!isset($test['multiple'])) { + $this->_invalidTagOrder($unfoundtags, $key, $root); + return false; + } + continue; + } else { + $unfoundtags = $optionaltags = array(); + $mismatch = false; + } + $key = next($keys); + while ($key == 'attribs' || $key == '_contents') { + $key = next($keys); + } + if ($key && $key != $test['tag'] && isset($test['multiple'])) { + $unfoundtags[] = $test['tag']; + $optionaltags[] = $test['tag']; + $mismatch = true; + } + $ret &= $this->_processAttribs($test, $tag, $root); + continue; + } + } + if (!$mismatch && count($optionaltags)) { + // don't error out on any optional tags + $unfoundtags = array_diff($unfoundtags, $optionaltags); + } + if (count($unfoundtags)) { + $this->_invalidTagOrder($unfoundtags, $key, $root); + } elseif ($key) { + // unknown tags + $this->_invalidTagOrder('*no tags allowed here*', $key, $root); + while ($key = next($keys)) { + $this->_invalidTagOrder('*no tags allowed here*', $key, $root); + } + } + return $ret; + } + + function _processAttribs($choice, $tag, $context) + { + if (isset($choice['attribs'])) { + if (!is_array($tag)) { + $tag = array($tag); + } + $tags = $tag; + if (!isset($tags[0])) { + $tags = array($tags); + } + $ret = true; + foreach ($tags as $i => $tag) { + if (!is_array($tag) || !isset($tag['attribs'])) { + foreach ($choice['attribs'] as $attrib) { + if ($attrib{0} != '?') { + $ret &= $this->_tagHasNoAttribs($choice['tag'], + $context); + continue 2; + } + } + } + foreach ($choice['attribs'] as $attrib) { + if ($attrib{0} != '?') { + if (!isset($tag['attribs'][$attrib])) { + $ret &= $this->_tagMissingAttribute($choice['tag'], + $attrib, $context); + } + } + } + } + return $ret; + } + return true; + } + + function _processStructure($key) + { + $ret = array(); + if (count($pieces = explode('|', $key)) > 1) { + $ret['choices'] = array(); + foreach ($pieces as $piece) { + $ret['choices'][] = $this->_processStructure($piece); + } + return $ret; + } + $multi = $key{0}; + if ($multi == '+' || $multi == '*') { + $ret['multiple'] = $key{0}; + $key = substr($key, 1); + } + if (count($attrs = explode('->', $key)) > 1) { + $ret['tag'] = array_shift($attrs); + $ret['attribs'] = $attrs; + } else { + $ret['tag'] = $key; + } + return $ret; + } + + function _validateStabilityVersion() + { + $structure = array('release', 'api'); + $a = $this->_stupidSchemaValidate($structure, $this->_packageInfo['version'], ''); + $a &= $this->_stupidSchemaValidate($structure, $this->_packageInfo['stability'], ''); + if ($a) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $this->_packageInfo['version']['release'])) { + $this->_invalidVersion('release', $this->_packageInfo['version']['release']); + } + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $this->_packageInfo['version']['api'])) { + $this->_invalidVersion('api', $this->_packageInfo['version']['api']); + } + if (!in_array($this->_packageInfo['stability']['release'], + array('snapshot', 'devel', 'alpha', 'beta', 'stable'))) { + $this->_invalidState('release', $this->_packageinfo['stability']['release']); + } + if (!in_array($this->_packageInfo['stability']['api'], + array('devel', 'alpha', 'beta', 'stable'))) { + $this->_invalidState('api', $this->_packageinfo['stability']['api']); + } + } + } + + function _validateMaintainers() + { + $structure = + array( + 'name', + 'user', + 'email', + 'active', + ); + foreach (array('lead', 'developer', 'contributor', 'helper') as $type) { + if (!isset($this->_packageInfo[$type])) { + continue; + } + if (isset($this->_packageInfo[$type][0])) { + foreach ($this->_packageInfo[$type] as $lead) { + $this->_stupidSchemaValidate($structure, $lead, '<' . $type . '>'); + } + } else { + $this->_stupidSchemaValidate($structure, $this->_packageInfo[$type], + '<' . $type . '>'); + } + } + } + + function _validatePhpDep($dep, $installcondition = false) + { + $structure = array( + 'min', + '*max', + '*exclude', + ); + $type = $installcondition ? '' : ''; + $this->_stupidSchemaValidate($structure, $dep, $type); + if (isset($dep['min'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?(?:-[a-zA-Z0-9]+)?\\z/', + $dep['min'])) { + $this->_invalidVersion($type . '', $dep['min']); + } + } + if (isset($dep['max'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?(?:-[a-zA-Z0-9]+)?\\z/', + $dep['max'])) { + $this->_invalidVersion($type . '', $dep['max']); + } + } + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + foreach ($dep['exclude'] as $exclude) { + if (!preg_match( + '/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?(?:-[a-zA-Z0-9]+)?\\z/', + $exclude)) { + $this->_invalidVersion($type . '', $exclude); + } + } + } + } + + function _validatePearinstallerDep($dep) + { + $structure = array( + 'min', + '*max', + '*recommended', + '*exclude', + ); + $this->_stupidSchemaValidate($structure, $dep, ''); + if (isset($dep['min'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['min'])) { + $this->_invalidVersion('', + $dep['min']); + } + } + if (isset($dep['max'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['max'])) { + $this->_invalidVersion('', + $dep['max']); + } + } + if (isset($dep['recommended'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['recommended'])) { + $this->_invalidVersion('', + $dep['recommended']); + } + } + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + foreach ($dep['exclude'] as $exclude) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $exclude)) { + $this->_invalidVersion('', + $exclude); + } + } + } + } + + function _validatePackageDep($dep, $group, $type = '') + { + if (isset($dep['uri'])) { + if (isset($dep['conflicts'])) { + $structure = array( + 'name', + 'uri', + 'conflicts', + '*providesextension', + ); + } else { + $structure = array( + 'name', + 'uri', + '*providesextension', + ); + } + } else { + if (isset($dep['conflicts'])) { + $structure = array( + 'name', + 'channel', + '*min', + '*max', + '*exclude', + 'conflicts', + '*providesextension', + ); + } else { + $structure = array( + 'name', + 'channel', + '*min', + '*max', + '*recommended', + '*exclude', + '*nodefault', + '*providesextension', + ); + } + } + if (isset($dep['name'])) { + $type .= '' . $dep['name'] . ''; + } + $this->_stupidSchemaValidate($structure, $dep, '' . $group . $type); + if (isset($dep['uri']) && (isset($dep['min']) || isset($dep['max']) || + isset($dep['recommended']) || isset($dep['exclude']))) { + $this->_uriDepsCannotHaveVersioning('' . $group . $type); + } + if (isset($dep['channel']) && strtolower($dep['channel']) == '__uri') { + $this->_DepchannelCannotBeUri('' . $group . $type); + } + if (isset($dep['min'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['min'])) { + $this->_invalidVersion('' . $group . $type . '', $dep['min']); + } + } + if (isset($dep['max'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['max'])) { + $this->_invalidVersion('' . $group . $type . '', $dep['max']); + } + } + if (isset($dep['recommended'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['recommended'])) { + $this->_invalidVersion('' . $group . $type . '', + $dep['recommended']); + } + } + if (isset($dep['exclude'])) { + if (!is_array($dep['exclude'])) { + $dep['exclude'] = array($dep['exclude']); + } + foreach ($dep['exclude'] as $exclude) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $exclude)) { + $this->_invalidVersion('' . $group . $type . '', + $exclude); + } + } + } + } + + function _validateSubpackageDep($dep, $group) + { + $this->_validatePackageDep($dep, $group, ''); + if (isset($dep['providesextension'])) { + $this->_subpackageCannotProvideExtension(isset($dep['name']) ? $dep['name'] : ''); + } + if (isset($dep['conflicts'])) { + $this->_subpackagesCannotConflict(isset($dep['name']) ? $dep['name'] : ''); + } + } + + function _validateExtensionDep($dep, $group = false, $installcondition = false) + { + if (isset($dep['conflicts'])) { + $structure = array( + 'name', + '*min', + '*max', + '*exclude', + 'conflicts', + ); + } else { + $structure = array( + 'name', + '*min', + '*max', + '*recommended', + '*exclude', + ); + } + if ($installcondition) { + $type = ''; + } else { + $type = '' . $group . ''; + } + if (isset($dep['name'])) { + $type .= '' . $dep['name'] . ''; + } + $this->_stupidSchemaValidate($structure, $dep, $type); + if (isset($dep['min'])) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $dep['min'])) { + $this->_invalidVersion(substr($type, 1) . '_invalidVersion(substr($type, 1) . '_invalidVersion(substr($type, 1) . '_invalidVersion(substr($type, 1) . '' : ''; + if ($this->_stupidSchemaValidate($structure, $dep, $type)) { + if ($dep['name'] == '*') { + if (array_key_exists('conflicts', $dep)) { + $this->_cannotConflictWithAllOs($type); + } + } + } + } + + function _validateArchDep($dep, $installcondition = false) + { + $structure = array( + 'pattern', + '*conflicts', + ); + $type = $installcondition ? '' : ''; + $this->_stupidSchemaValidate($structure, $dep, $type); + } + + function _validateInstallConditions($cond, $release) + { + $structure = array( + '*php', + '*extension', + '*os', + '*arch', + ); + if (!$this->_stupidSchemaValidate($structure, + $cond, $release)) { + return false; + } + foreach (array('php', 'extension', 'os', 'arch') as $type) { + if (isset($cond[$type])) { + $iter = $cond[$type]; + if (!is_array($iter) || !isset($iter[0])) { + $iter = array($iter); + } + foreach ($iter as $package) { + if ($type == 'extension') { + $this->{"_validate{$type}Dep"}($package, false, true); + } else { + $this->{"_validate{$type}Dep"}($package, true); + } + } + } + } + } + + function _validateDependencies() + { + $structure = array( + 'required', + '*optional', + '*group->name->hint' + ); + if (!$this->_stupidSchemaValidate($structure, + $this->_packageInfo['dependencies'], '')) { + return false; + } + foreach (array('required', 'optional') as $simpledep) { + if (isset($this->_packageInfo['dependencies'][$simpledep])) { + if ($simpledep == 'optional') { + $structure = array( + '*package', + '*subpackage', + '*extension', + ); + } else { + $structure = array( + 'php', + 'pearinstaller', + '*package', + '*subpackage', + '*extension', + '*os', + '*arch', + ); + } + if ($this->_stupidSchemaValidate($structure, + $this->_packageInfo['dependencies'][$simpledep], + "<$simpledep>")) { + foreach (array('package', 'subpackage', 'extension') as $type) { + if (isset($this->_packageInfo['dependencies'][$simpledep][$type])) { + $iter = $this->_packageInfo['dependencies'][$simpledep][$type]; + if (!isset($iter[0])) { + $iter = array($iter); + } + foreach ($iter as $package) { + if ($type != 'extension') { + if (isset($package['uri'])) { + if (isset($package['channel'])) { + $this->_UrlOrChannel($type, + $package['name']); + } + } else { + if (!isset($package['channel'])) { + $this->_NoChannel($type, $package['name']); + } + } + } + $this->{"_validate{$type}Dep"}($package, "<$simpledep>"); + } + } + } + if ($simpledep == 'optional') { + continue; + } + foreach (array('php', 'pearinstaller', 'os', 'arch') as $type) { + if (isset($this->_packageInfo['dependencies'][$simpledep][$type])) { + $iter = $this->_packageInfo['dependencies'][$simpledep][$type]; + if (!isset($iter[0])) { + $iter = array($iter); + } + foreach ($iter as $package) { + $this->{"_validate{$type}Dep"}($package); + } + } + } + } + } + } + if (isset($this->_packageInfo['dependencies']['group'])) { + $groups = $this->_packageInfo['dependencies']['group']; + if (!isset($groups[0])) { + $groups = array($groups); + } + $structure = array( + '*package', + '*subpackage', + '*extension', + ); + foreach ($groups as $group) { + if ($this->_stupidSchemaValidate($structure, $group, '')) { + if (!PEAR_Validate::validGroupName($group['attribs']['name'])) { + $this->_invalidDepGroupName($group['attribs']['name']); + } + foreach (array('package', 'subpackage', 'extension') as $type) { + if (isset($group[$type])) { + $iter = $group[$type]; + if (!isset($iter[0])) { + $iter = array($iter); + } + foreach ($iter as $package) { + if ($type != 'extension') { + if (isset($package['uri'])) { + if (isset($package['channel'])) { + $this->_UrlOrChannelGroup($type, + $package['name'], + $group['name']); + } + } else { + if (!isset($package['channel'])) { + $this->_NoChannelGroup($type, + $package['name'], + $group['name']); + } + } + } + $this->{"_validate{$type}Dep"}($package, ''); + } + } + } + } + } + } + } + + function _validateCompatible() + { + $compat = $this->_packageInfo['compatible']; + if (!isset($compat[0])) { + $compat = array($compat); + } + $required = array('name', 'channel', 'min', 'max', '*exclude'); + foreach ($compat as $package) { + $type = ''; + if (is_array($package) && array_key_exists('name', $package)) { + $type .= '' . $package['name'] . ''; + } + $this->_stupidSchemaValidate($required, $package, $type); + if (is_array($package) && array_key_exists('min', $package)) { + if (!preg_match('/^\d+(?:\.\d+)*(?:[a-zA-Z]+\d*)?\\z/', + $package['min'])) { + $this->_invalidVersion(substr($type, 1) . '_invalidVersion(substr($type, 1) . '_invalidVersion(substr($type, 1) . '_NoBundledPackages(); + } + if (!is_array($list['bundledpackage']) || !isset($list['bundledpackage'][0])) { + return $this->_AtLeast2BundledPackages(); + } + foreach ($list['bundledpackage'] as $package) { + if (!is_string($package)) { + $this->_bundledPackagesMustBeFilename(); + } + } + } + + function _validateFilelist($list = false, $allowignore = false, $dirs = '') + { + $iscontents = false; + if (!$list) { + $iscontents = true; + $list = $this->_packageInfo['contents']; + if (isset($this->_packageInfo['bundle'])) { + return $this->_validateBundle($list); + } + } + if ($allowignore) { + $struc = array( + '*install->name->as', + '*ignore->name' + ); + } else { + $struc = array( + '*dir->name->?baseinstalldir', + '*file->name->role->?baseinstalldir->?md5sum' + ); + if (isset($list['dir']) && isset($list['file'])) { + // stave off validation errors without requiring a set order. + $_old = $list; + if (isset($list['attribs'])) { + $list = array('attribs' => $_old['attribs']); + } + $list['dir'] = $_old['dir']; + $list['file'] = $_old['file']; + } + } + if (!isset($list['attribs']) || !isset($list['attribs']['name'])) { + $unknown = $allowignore ? '' : ''; + $dirname = $iscontents ? '' : $unknown; + } else { + $dirname = ''; + if (preg_match('~/\.\.?(/|\\z)|^\.\.?/~', + str_replace('\\', '/', $list['attribs']['name']))) { + // file contains .. parent directory or . cur directory + $this->_invalidDirName($list['attribs']['name']); + } + } + $res = $this->_stupidSchemaValidate($struc, $list, $dirname); + if ($allowignore && $res) { + $ignored_or_installed = array(); + $this->_pf->getFilelist(); + $fcontents = $this->_pf->getContents(); + $filelist = array(); + if (!isset($fcontents['dir']['file'][0])) { + $fcontents['dir']['file'] = array($fcontents['dir']['file']); + } + foreach ($fcontents['dir']['file'] as $file) { + $filelist[$file['attribs']['name']] = true; + } + if (isset($list['install'])) { + if (!isset($list['install'][0])) { + $list['install'] = array($list['install']); + } + foreach ($list['install'] as $file) { + if (!isset($filelist[$file['attribs']['name']])) { + $this->_notInContents($file['attribs']['name'], 'install'); + continue; + } + if (array_key_exists($file['attribs']['name'], $ignored_or_installed)) { + $this->_multipleInstallAs($file['attribs']['name']); + } + if (!isset($ignored_or_installed[$file['attribs']['name']])) { + $ignored_or_installed[$file['attribs']['name']] = array(); + } + $ignored_or_installed[$file['attribs']['name']][] = 1; + if (preg_match('~/\.\.?(/|\\z)|^\.\.?/~', + str_replace('\\', '/', $file['attribs']['as']))) { + // file contains .. parent directory or . cur directory references + $this->_invalidFileInstallAs($file['attribs']['name'], + $file['attribs']['as']); + } + } + } + if (isset($list['ignore'])) { + if (!isset($list['ignore'][0])) { + $list['ignore'] = array($list['ignore']); + } + foreach ($list['ignore'] as $file) { + if (!isset($filelist[$file['attribs']['name']])) { + $this->_notInContents($file['attribs']['name'], 'ignore'); + continue; + } + if (array_key_exists($file['attribs']['name'], $ignored_or_installed)) { + $this->_ignoreAndInstallAs($file['attribs']['name']); + } + } + } + } + if (!$allowignore && isset($list['file'])) { + if (is_string($list['file'])) { + $this->_oldStyleFileNotAllowed(); + return false; + } + if (!isset($list['file'][0])) { + // single file + $list['file'] = array($list['file']); + } + foreach ($list['file'] as $i => $file) + { + if (isset($file['attribs']) && isset($file['attribs']['name'])) { + if ($file['attribs']['name']{0} == '.' && + $file['attribs']['name']{1} == '/') { + // name is something like "./doc/whatever.txt" + $this->_invalidFileName($file['attribs']['name'], $dirname); + } + if (preg_match('~/\.\.?(/|\\z)|^\.\.?/~', + str_replace('\\', '/', $file['attribs']['name']))) { + // file contains .. parent directory or . cur directory + $this->_invalidFileName($file['attribs']['name'], $dirname); + } + } + if (isset($file['attribs']) && isset($file['attribs']['role'])) { + if (!$this->_validateRole($file['attribs']['role'])) { + if (isset($this->_packageInfo['usesrole'])) { + $roles = $this->_packageInfo['usesrole']; + if (!isset($roles[0])) { + $roles = array($roles); + } + foreach ($roles as $role) { + if ($role['role'] = $file['attribs']['role']) { + $msg = 'This package contains role "%role%" and requires ' . + 'package "%package%" to be used'; + if (isset($role['uri'])) { + $params = array('role' => $role['role'], + 'package' => $role['uri']); + } else { + $params = array('role' => $role['role'], + 'package' => $this->_pf->_registry-> + parsedPackageNameToString(array('package' => + $role['package'], 'channel' => $role['channel']), + true)); + } + $this->_stack->push('_mustInstallRole', 'error', $params, $msg); + } + } + } + $this->_invalidFileRole($file['attribs']['name'], + $dirname, $file['attribs']['role']); + } + } + if (!isset($file['attribs'])) { + continue; + } + $save = $file['attribs']; + if ($dirs) { + $save['name'] = $dirs . '/' . $save['name']; + } + unset($file['attribs']); + if (count($file) && $this->_curState != PEAR_VALIDATE_DOWNLOADING) { // has tasks + foreach ($file as $task => $value) { + if ($tagClass = $this->_pf->getTask($task)) { + if (!is_array($value) || !isset($value[0])) { + $value = array($value); + } + foreach ($value as $v) { + $ret = call_user_func(array($tagClass, 'validateXml'), + $this->_pf, $v, $this->_pf->_config, $save); + if (is_array($ret)) { + $this->_invalidTask($task, $ret, isset($save['name']) ? + $save['name'] : ''); + } + } + } else { + if (isset($this->_packageInfo['usestask'])) { + $roles = $this->_packageInfo['usestask']; + if (!isset($roles[0])) { + $roles = array($roles); + } + foreach ($roles as $role) { + if ($role['task'] = $task) { + $msg = 'This package contains task "%task%" and requires ' . + 'package "%package%" to be used'; + if (isset($role['uri'])) { + $params = array('task' => $role['task'], + 'package' => $role['uri']); + } else { + $params = array('task' => $role['task'], + 'package' => $this->_pf->_registry-> + parsedPackageNameToString(array('package' => + $role['package'], 'channel' => $role['channel']), + true)); + } + $this->_stack->push('_mustInstallTask', 'error', + $params, $msg); + } + } + } + $this->_unknownTask($task, $save['name']); + } + } + } + } + } + if (isset($list['ignore'])) { + if (!$allowignore) { + $this->_ignoreNotAllowed('ignore'); + } + } + if (isset($list['install'])) { + if (!$allowignore) { + $this->_ignoreNotAllowed('install'); + } + } + if (isset($list['file'])) { + if ($allowignore) { + $this->_fileNotAllowed('file'); + } + } + if (isset($list['dir'])) { + if ($allowignore) { + $this->_fileNotAllowed('dir'); + } else { + if (!isset($list['dir'][0])) { + $list['dir'] = array($list['dir']); + } + foreach ($list['dir'] as $dir) { + if (isset($dir['attribs']) && isset($dir['attribs']['name'])) { + if ($dir['attribs']['name'] == '/' || + !isset($this->_packageInfo['contents']['dir']['dir'])) { + // always use nothing if the filelist has already been flattened + $newdirs = ''; + } elseif ($dirs == '') { + $newdirs = $dir['attribs']['name']; + } else { + $newdirs = $dirs . '/' . $dir['attribs']['name']; + } + } else { + $newdirs = $dirs; + } + $this->_validateFilelist($dir, $allowignore, $newdirs); + } + } + } + } + + function _validateRelease() + { + if (isset($this->_packageInfo['phprelease'])) { + $release = 'phprelease'; + if (isset($this->_packageInfo['providesextension'])) { + $this->_cannotProvideExtension($release); + } + if (isset($this->_packageInfo['srcpackage']) || isset($this->_packageInfo['srcuri'])) { + $this->_cannotHaveSrcpackage($release); + } + $releases = $this->_packageInfo['phprelease']; + if (!is_array($releases)) { + return true; + } + if (!isset($releases[0])) { + $releases = array($releases); + } + foreach ($releases as $rel) { + $this->_stupidSchemaValidate(array( + '*installconditions', + '*filelist', + ), $rel, ''); + } + } + foreach (array('', 'zend') as $prefix) { + $releasetype = $prefix . 'extsrcrelease'; + if (isset($this->_packageInfo[$releasetype])) { + $release = $releasetype; + if (!isset($this->_packageInfo['providesextension'])) { + $this->_mustProvideExtension($release); + } + if (isset($this->_packageInfo['srcpackage']) || isset($this->_packageInfo['srcuri'])) { + $this->_cannotHaveSrcpackage($release); + } + $releases = $this->_packageInfo[$releasetype]; + if (!is_array($releases)) { + return true; + } + if (!isset($releases[0])) { + $releases = array($releases); + } + foreach ($releases as $rel) { + $this->_stupidSchemaValidate(array( + '*installconditions', + '*configureoption->name->prompt->?default', + '*binarypackage', + '*filelist', + ), $rel, '<' . $releasetype . '>'); + if (isset($rel['binarypackage'])) { + if (!is_array($rel['binarypackage']) || !isset($rel['binarypackage'][0])) { + $rel['binarypackage'] = array($rel['binarypackage']); + } + foreach ($rel['binarypackage'] as $bin) { + if (!is_string($bin)) { + $this->_binaryPackageMustBePackagename(); + } + } + } + } + } + $releasetype = 'extbinrelease'; + if (isset($this->_packageInfo[$releasetype])) { + $release = $releasetype; + if (!isset($this->_packageInfo['providesextension'])) { + $this->_mustProvideExtension($release); + } + if (isset($this->_packageInfo['channel']) && + !isset($this->_packageInfo['srcpackage'])) { + $this->_mustSrcPackage($release); + } + if (isset($this->_packageInfo['uri']) && !isset($this->_packageInfo['srcuri'])) { + $this->_mustSrcuri($release); + } + $releases = $this->_packageInfo[$releasetype]; + if (!is_array($releases)) { + return true; + } + if (!isset($releases[0])) { + $releases = array($releases); + } + foreach ($releases as $rel) { + $this->_stupidSchemaValidate(array( + '*installconditions', + '*filelist', + ), $rel, '<' . $releasetype . '>'); + } + } + } + if (isset($this->_packageInfo['bundle'])) { + $release = 'bundle'; + if (isset($this->_packageInfo['providesextension'])) { + $this->_cannotProvideExtension($release); + } + if (isset($this->_packageInfo['srcpackage']) || isset($this->_packageInfo['srcuri'])) { + $this->_cannotHaveSrcpackage($release); + } + $releases = $this->_packageInfo['bundle']; + if (!is_array($releases) || !isset($releases[0])) { + $releases = array($releases); + } + foreach ($releases as $rel) { + $this->_stupidSchemaValidate(array( + '*installconditions', + '*filelist', + ), $rel, ''); + } + } + foreach ($releases as $rel) { + if (is_array($rel) && array_key_exists('installconditions', $rel)) { + $this->_validateInstallConditions($rel['installconditions'], + "<$release>"); + } + if (is_array($rel) && array_key_exists('filelist', $rel)) { + if ($rel['filelist']) { + + $this->_validateFilelist($rel['filelist'], true); + } + } + } + } + + /** + * This is here to allow role extension through plugins + * @param string + */ + function _validateRole($role) + { + return in_array($role, PEAR_Installer_Role::getValidRoles($this->_pf->getPackageType())); + } + + function _pearVersionTooLow($version) + { + $this->_stack->push(__FUNCTION__, 'error', + array('version' => $version), + 'This package.xml requires PEAR version %version% to parse properly, we are ' . + 'version 1.6.1'); + } + + function _invalidTagOrder($oktags, $actual, $root) + { + $this->_stack->push(__FUNCTION__, 'error', + array('oktags' => $oktags, 'actual' => $actual, 'root' => $root), + 'Invalid tag order in %root%, found <%actual%> expected one of "%oktags%"'); + } + + function _ignoreNotAllowed($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type), + '<%type%> is not allowed inside global , only inside ' . + '//, use and only'); + } + + function _fileNotAllowed($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type), + '<%type%> is not allowed inside release , only inside ' . + ', use and only'); + } + + function _oldStyleFileNotAllowed() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'Old-style name is not allowed. Use' . + ''); + } + + function _tagMissingAttribute($tag, $attr, $context) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $tag, + 'attribute' => $attr, 'context' => $context), + 'tag <%tag%> in context "%context%" has no attribute "%attribute%"'); + } + + function _tagHasNoAttribs($tag, $context) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $tag, + 'context' => $context), + 'tag <%tag%> has no attributes in context "%context%"'); + } + + function _invalidInternalStructure() + { + $this->_stack->push(__FUNCTION__, 'exception', array(), + 'internal array was not generated by compatible parser, or extreme parser error, cannot continue'); + } + + function _invalidFileRole($file, $dir, $role) + { + $this->_stack->push(__FUNCTION__, 'error', array( + 'file' => $file, 'dir' => $dir, 'role' => $role, + 'roles' => PEAR_Installer_Role::getValidRoles($this->_pf->getPackageType())), + 'File "%file%" in directory "%dir%" has invalid role "%role%", should be one of %roles%'); + } + + function _invalidFileName($file, $dir) + { + $this->_stack->push(__FUNCTION__, 'error', array( + 'file' => $file), + 'File "%file%" in directory "%dir%" cannot begin with "./" or contain ".."'); + } + + function _invalidFileInstallAs($file, $as) + { + $this->_stack->push(__FUNCTION__, 'error', array( + 'file' => $file, 'as' => $as), + 'File "%file%" cannot contain "./" or contain ".."'); + } + + function _invalidDirName($dir) + { + $this->_stack->push(__FUNCTION__, 'error', array( + 'dir' => $file), + 'Directory "%dir%" cannot begin with "./" or contain ".."'); + } + + function _filelistCannotContainFile($filelist) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $filelist), + '<%tag%> can only contain , contains . Use ' . + ' as the first dir element'); + } + + function _filelistMustContainDir($filelist) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $filelist), + '<%tag%> must contain . Use as the ' . + 'first dir element'); + } + + function _tagCannotBeEmpty($tag) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $tag), + '<%tag%> cannot be empty (<%tag%/>)'); + } + + function _UrlOrChannel($type, $name) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, + 'name' => $name), + 'Required dependency <%type%> "%name%" can have either url OR ' . + 'channel attributes, and not both'); + } + + function _NoChannel($type, $name) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, + 'name' => $name), + 'Required dependency <%type%> "%name%" must have either url OR ' . + 'channel attributes'); + } + + function _UrlOrChannelGroup($type, $name, $group) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, + 'name' => $name, 'group' => $group), + 'Group "%group%" dependency <%type%> "%name%" can have either url OR ' . + 'channel attributes, and not both'); + } + + function _NoChannelGroup($type, $name, $group) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, + 'name' => $name, 'group' => $group), + 'Group "%group%" dependency <%type%> "%name%" must have either url OR ' . + 'channel attributes'); + } + + function _unknownChannel($channel) + { + $this->_stack->push(__FUNCTION__, 'error', array('channel' => $channel), + 'Unknown channel "%channel%"'); + } + + function _noPackageVersion() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'package.xml tag has no version attribute, or version is not 2.0'); + } + + function _NoBundledPackages() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'No tag was found in , required for bundle packages'); + } + + function _AtLeast2BundledPackages() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'At least 2 packages must be bundled in a bundle package'); + } + + function _ChannelOrUri($name) + { + $this->_stack->push(__FUNCTION__, 'error', array('name' => $name), + 'Bundled package "%name%" can have either a uri or a channel, not both'); + } + + function _noChildTag($child, $tag) + { + $this->_stack->push(__FUNCTION__, 'error', array('child' => $child, 'tag' => $tag), + 'Tag <%tag%> is missing child tag <%child%>'); + } + + function _invalidVersion($type, $value) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, 'value' => $value), + 'Version type <%type%> is not a valid version (%value%)'); + } + + function _invalidState($type, $value) + { + $states = array('stable', 'beta', 'alpha', 'devel'); + if ($type != 'api') { + $states[] = 'snapshot'; + } + if (strtolower($value) == 'rc') { + $this->_stack->push(__FUNCTION__, 'error', + array('version' => $this->_packageInfo['version']['release']), + 'RC is not a state, it is a version postfix, try %version%RC1, stability beta'); + } + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type, 'value' => $value, + 'types' => $states), + 'Stability type <%type%> is not a valid stability (%value%), must be one of ' . + '%types%'); + } + + function _invalidTask($task, $ret, $file) + { + switch ($ret[0]) { + case PEAR_TASK_ERROR_MISSING_ATTRIB : + $info = array('attrib' => $ret[1], 'task' => $task, 'file' => $file); + $msg = 'task <%task%> is missing attribute "%attrib%" in file %file%'; + break; + case PEAR_TASK_ERROR_NOATTRIBS : + $info = array('task' => $task, 'file' => $file); + $msg = 'task <%task%> has no attributes in file %file%'; + break; + case PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE : + $info = array('attrib' => $ret[1], 'values' => $ret[3], + 'was' => $ret[2], 'task' => $task, 'file' => $file); + $msg = 'task <%task%> attribute "%attrib%" has the wrong value "%was%" '. + 'in file %file%, expecting one of "%values%"'; + break; + case PEAR_TASK_ERROR_INVALID : + $info = array('reason' => $ret[1], 'task' => $task, 'file' => $file); + $msg = 'task <%task%> in file %file% is invalid because of "%reason%"'; + break; + } + $this->_stack->push(__FUNCTION__, 'error', $info, $msg); + } + + function _unknownTask($task, $file) + { + $this->_stack->push(__FUNCTION__, 'error', array('task' => $task, 'file' => $file), + 'Unknown task "%task%" passed in file '); + } + + function _subpackageCannotProvideExtension($name) + { + $this->_stack->push(__FUNCTION__, 'error', array('name' => $name), + 'Subpackage dependency "%name%" cannot use , ' . + 'only package dependencies can use this tag'); + } + + function _subpackagesCannotConflict($name) + { + $this->_stack->push(__FUNCTION__, 'error', array('name' => $name), + 'Subpackage dependency "%name%" cannot use , ' . + 'only package dependencies can use this tag'); + } + + function _cannotProvideExtension($release) + { + $this->_stack->push(__FUNCTION__, 'error', array('release' => $release), + '<%release%> packages cannot use , only extbinrelease, extsrcrelease, zendextsrcrelease, and zendextbinrelease can provide a PHP extension'); + } + + function _mustProvideExtension($release) + { + $this->_stack->push(__FUNCTION__, 'error', array('release' => $release), + '<%release%> packages must use to indicate which PHP extension is provided'); + } + + function _cannotHaveSrcpackage($release) + { + $this->_stack->push(__FUNCTION__, 'error', array('release' => $release), + '<%release%> packages cannot specify a source code package, only extension binaries may use the tag'); + } + + function _mustSrcPackage($release) + { + $this->_stack->push(__FUNCTION__, 'error', array('release' => $release), + '/ packages must specify a source code package with '); + } + + function _mustSrcuri($release) + { + $this->_stack->push(__FUNCTION__, 'error', array('release' => $release), + '/ packages must specify a source code package with '); + } + + function _uriDepsCannotHaveVersioning($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type), + '%type%: dependencies with a tag cannot have any versioning information'); + } + + function _conflictingDepsCannotHaveVersioning($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type), + '%type%: conflicting dependencies cannot have versioning info, use to ' . + 'exclude specific versions of a dependency'); + } + + function _DepchannelCannotBeUri($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('type' => $type), + '%type%: channel cannot be __uri, this is a pseudo-channel reserved for uri ' . + 'dependencies only'); + } + + function _bundledPackagesMustBeFilename() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + ' tags must contain only the filename of a package release ' . + 'in the bundle'); + } + + function _binaryPackageMustBePackagename() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + ' tags must contain the name of a package that is ' . + 'a compiled version of this extsrc/zendextsrc package'); + } + + function _fileNotFound($file) + { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'File "%file%" in package.xml does not exist'); + } + + function _notInContents($file, $tag) + { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file, 'tag' => $tag), + '<%tag% name="%file%"> is invalid, file is not in '); + } + + function _cannotValidateNoPathSet() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'Cannot validate files, no path to package file is set (use setPackageFile())'); + } + + function _usesroletaskMustHaveChannelOrUri($role, $tag) + { + $this->_stack->push(__FUNCTION__, 'error', array('role' => $role, 'tag' => $tag), + '<%tag%> for role "%role%" must contain either , or and '); + } + + function _usesroletaskMustHavePackage($role, $tag) + { + $this->_stack->push(__FUNCTION__, 'error', array('role' => $role, 'tag' => $tag), + '<%tag%> for role "%role%" must contain '); + } + + function _usesroletaskMustHaveRoleTask($tag, $type) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $tag, 'type' => $type), + '<%tag%> must contain <%type%> defining the %type% to be used'); + } + + function _cannotConflictWithAllOs($type) + { + $this->_stack->push(__FUNCTION__, 'error', array('tag' => $tag), + '%tag% cannot conflict with all OSes'); + } + + function _invalidDepGroupName($name) + { + $this->_stack->push(__FUNCTION__, 'error', array('name' => $name), + 'Invalid dependency group name "%name%"'); + } + + function _multipleToplevelDirNotAllowed() + { + $this->_stack->push(__FUNCTION__, 'error', array(), + 'Multiple top-level tags are not allowed. Enclose them ' . + 'in a '); + } + + function _multipleInstallAs($file) + { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'Only one tag is allowed for file "%file%"'); + } + + function _ignoreAndInstallAs($file) + { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'Cannot have both and tags for file "%file%"'); + } + + function _analyzeBundledPackages() + { + if (!$this->_isValid) { + return false; + } + if (!$this->_pf->getPackageType() == 'bundle') { + return false; + } + if (!isset($this->_pf->_packageFile)) { + return false; + } + $dir_prefix = dirname($this->_pf->_packageFile); + $common = new PEAR_Common; + $log = isset($this->_pf->_logger) ? array(&$this->_pf->_logger, 'log') : + array($common, 'log'); + $info = $this->_pf->getContents(); + $info = $info['bundledpackage']; + if (!is_array($info)) { + $info = array($info); + } + $pkg = &new PEAR_PackageFile($this->_pf->_config); + foreach ($info as $package) { + if (!file_exists($dir_prefix . DIRECTORY_SEPARATOR . $package)) { + $this->_fileNotFound($dir_prefix . DIRECTORY_SEPARATOR . $package); + $this->_isValid = 0; + continue; + } + call_user_func_array($log, array(1, "Analyzing bundled package $package")); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $ret = $pkg->fromAnyFile($dir_prefix . DIRECTORY_SEPARATOR . $package, + PEAR_VALIDATE_NORMAL); + PEAR::popErrorHandling(); + if (PEAR::isError($ret)) { + call_user_func_array($log, array(0, "ERROR: package $package is not a valid " . + 'package')); + $inf = $ret->getUserInfo(); + if (is_array($inf)) { + foreach ($inf as $err) { + call_user_func_array($log, array(1, $err['message'])); + } + } + return false; + } + } + return true; + } + + function _analyzePhpFiles() + { + if (!$this->_isValid) { + return false; + } + if (!isset($this->_pf->_packageFile)) { + $this->_cannotValidateNoPathSet(); + return false; + } + $dir_prefix = dirname($this->_pf->_packageFile); + $common = new PEAR_Common; + $log = isset($this->_pf->_logger) ? array(&$this->_pf->_logger, 'log') : + array(&$common, 'log'); + $info = $this->_pf->getContents(); + if (!$info || !isset($info['dir']['file'])) { + $this->_tagCannotBeEmpty('contents>_fileNotFound($dir_prefix . DIRECTORY_SEPARATOR . $file); + $this->_isValid = 0; + continue; + } + if (in_array($fa['role'], PEAR_Installer_Role::getPhpRoles()) && $dir_prefix) { + call_user_func_array($log, array(1, "Analyzing $file")); + $srcinfo = $this->analyzeSourceCode($dir_prefix . DIRECTORY_SEPARATOR . $file); + if ($srcinfo) { + $provides = array_merge($provides, $this->_buildProvidesArray($srcinfo)); + } + } + } + $this->_packageName = $pn = $this->_pf->getPackage(); + $pnl = strlen($pn); + foreach ($provides as $key => $what) { + if (isset($what['explicit']) || !$what) { + // skip conformance checks if the provides entry is + // specified in the package.xml file + continue; + } + extract($what); + if ($type == 'class') { + if (!strncasecmp($name, $pn, $pnl)) { + continue; + } + $this->_stack->push(__FUNCTION__, 'warning', + array('file' => $file, 'type' => $type, 'name' => $name, 'package' => $pn), + 'in %file%: %type% "%name%" not prefixed with package name "%package%"'); + } elseif ($type == 'function') { + if (strstr($name, '::') || !strncasecmp($name, $pn, $pnl)) { + continue; + } + $this->_stack->push(__FUNCTION__, 'warning', + array('file' => $file, 'type' => $type, 'name' => $name, 'package' => $pn), + 'in %file%: %type% "%name%" not prefixed with package name "%package%"'); + } + } + return $this->_isValid; + } + + /** + * Analyze the source code of the given PHP file + * + * @param string Filename of the PHP file + * @param boolean whether to analyze $file as the file contents + * @return mixed + */ + function analyzeSourceCode($file, $string = false) + { + if (!function_exists("token_get_all")) { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'Parser error: token_get_all() function must exist to analyze source code, PHP may have been compiled with --disable-tokenizer'); + return false; + } + if (!defined('T_DOC_COMMENT')) { + define('T_DOC_COMMENT', T_COMMENT); + } + if (!defined('T_INTERFACE')) { + define('T_INTERFACE', -1); + } + if (!defined('T_IMPLEMENTS')) { + define('T_IMPLEMENTS', -1); + } + if ($string) { + $contents = $file; + } else { + if (!$fp = @fopen($file, "r")) { + return false; + } + fclose($fp); + $contents = file_get_contents($file); + } + $tokens = token_get_all($contents); +/* + for ($i = 0; $i < sizeof($tokens); $i++) { + @list($token, $data) = $tokens[$i]; + if (is_string($token)) { + var_dump($token); + } else { + print token_name($token) . ' '; + var_dump(rtrim($data)); + } + } +*/ + $look_for = 0; + $paren_level = 0; + $bracket_level = 0; + $brace_level = 0; + $lastphpdoc = ''; + $current_class = ''; + $current_interface = ''; + $current_class_level = -1; + $current_function = ''; + $current_function_level = -1; + $declared_classes = array(); + $declared_interfaces = array(); + $declared_functions = array(); + $declared_methods = array(); + $used_classes = array(); + $used_functions = array(); + $extends = array(); + $implements = array(); + $nodeps = array(); + $inquote = false; + $interface = false; + for ($i = 0; $i < sizeof($tokens); $i++) { + if (is_array($tokens[$i])) { + list($token, $data) = $tokens[$i]; + } else { + $token = $tokens[$i]; + $data = ''; + } + if ($inquote) { + if ($token != '"' && $token != T_END_HEREDOC) { + continue; + } else { + $inquote = false; + continue; + } + } + switch ($token) { + case T_WHITESPACE : + continue; + case ';': + if ($interface) { + $current_function = ''; + $current_function_level = -1; + } + break; + case '"': + case T_START_HEREDOC: + $inquote = true; + break; + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + case '{': $brace_level++; continue 2; + case '}': + $brace_level--; + if ($current_class_level == $brace_level) { + $current_class = ''; + $current_class_level = -1; + } + if ($current_function_level == $brace_level) { + $current_function = ''; + $current_function_level = -1; + } + continue 2; + case '[': $bracket_level++; continue 2; + case ']': $bracket_level--; continue 2; + case '(': $paren_level++; continue 2; + case ')': $paren_level--; continue 2; + case T_INTERFACE: + $interface = true; + case T_CLASS: + if (($current_class_level != -1) || ($current_function_level != -1)) { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'Parser error: invalid PHP found in file "%file%"'); + return false; + } + case T_FUNCTION: + case T_NEW: + case T_EXTENDS: + case T_IMPLEMENTS: + $look_for = $token; + continue 2; + case T_STRING: + if (version_compare(zend_version(), '2.0', '<')) { + if (in_array(strtolower($data), + array('public', 'private', 'protected', 'abstract', + 'interface', 'implements', 'throw') + )) { + $this->_stack->push(__FUNCTION__, 'warning', array( + 'file' => $file), + 'Error, PHP5 token encountered in %file%,' . + ' analysis should be in PHP5'); + } + } + if ($look_for == T_CLASS) { + $current_class = $data; + $current_class_level = $brace_level; + $declared_classes[] = $current_class; + } elseif ($look_for == T_INTERFACE) { + $current_interface = $data; + $current_class_level = $brace_level; + $declared_interfaces[] = $current_interface; + } elseif ($look_for == T_IMPLEMENTS) { + $implements[$current_class] = $data; + } elseif ($look_for == T_EXTENDS) { + $extends[$current_class] = $data; + } elseif ($look_for == T_FUNCTION) { + if ($current_class) { + $current_function = "$current_class::$data"; + $declared_methods[$current_class][] = $data; + } elseif ($current_interface) { + $current_function = "$current_interface::$data"; + $declared_methods[$current_interface][] = $data; + } else { + $current_function = $data; + $declared_functions[] = $current_function; + } + $current_function_level = $brace_level; + $m = array(); + } elseif ($look_for == T_NEW) { + $used_classes[$data] = true; + } + $look_for = 0; + continue 2; + case T_VARIABLE: + $look_for = 0; + continue 2; + case T_DOC_COMMENT: + case T_COMMENT: + if (preg_match('!^/\*\*\s!', $data)) { + $lastphpdoc = $data; + if (preg_match_all('/@nodep\s+(\S+)/', $lastphpdoc, $m)) { + $nodeps = array_merge($nodeps, $m[1]); + } + } + continue 2; + case T_DOUBLE_COLON: + if (!($tokens[$i - 1][0] == T_WHITESPACE || $tokens[$i - 1][0] == T_STRING)) { + $this->_stack->push(__FUNCTION__, 'warning', array('file' => $file), + 'Parser error: invalid PHP found in file "%file%"'); + return false; + } + $class = $tokens[$i - 1][1]; + if (strtolower($class) != 'parent') { + $used_classes[$class] = true; + } + continue 2; + } + } + return array( + "source_file" => $file, + "declared_classes" => $declared_classes, + "declared_interfaces" => $declared_interfaces, + "declared_methods" => $declared_methods, + "declared_functions" => $declared_functions, + "used_classes" => array_diff(array_keys($used_classes), $nodeps), + "inheritance" => $extends, + "implements" => $implements, + ); + } + + /** + * Build a "provides" array from data returned by + * analyzeSourceCode(). The format of the built array is like + * this: + * + * array( + * 'class;MyClass' => 'array('type' => 'class', 'name' => 'MyClass'), + * ... + * ) + * + * + * @param array $srcinfo array with information about a source file + * as returned by the analyzeSourceCode() method. + * + * @return void + * + * @access private + * + */ + function _buildProvidesArray($srcinfo) + { + if (!$this->_isValid) { + return array(); + } + $providesret = array(); + $file = basename($srcinfo['source_file']); + $pn = $this->_pf->getPackage(); + $pnl = strlen($pn); + foreach ($srcinfo['declared_classes'] as $class) { + $key = "class;$class"; + if (isset($providesret[$key])) { + continue; + } + $providesret[$key] = + array('file'=> $file, 'type' => 'class', 'name' => $class); + if (isset($srcinfo['inheritance'][$class])) { + $providesret[$key]['extends'] = + $srcinfo['inheritance'][$class]; + } + } + foreach ($srcinfo['declared_methods'] as $class => $methods) { + foreach ($methods as $method) { + $function = "$class::$method"; + $key = "function;$function"; + if ($method{0} == '_' || !strcasecmp($method, $class) || + isset($providesret[$key])) { + continue; + } + $providesret[$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + } + + foreach ($srcinfo['declared_functions'] as $function) { + $key = "function;$function"; + if ($function{0} == '_' || isset($providesret[$key])) { + continue; + } + if (!strstr($function, '::') && strncasecmp($function, $pn, $pnl)) { + $warnings[] = "in1 " . $file . ": function \"$function\" not prefixed with package name \"$pn\""; + } + $providesret[$key] = + array('file'=> $file, 'type' => 'function', 'name' => $function); + } + return $providesret; + } +} +?> \ No newline at end of file diff --git a/PEAR/PackageFile/v2/rw.php b/PEAR/PackageFile/v2/rw.php new file mode 100644 index 0000000..7f74ad7 --- /dev/null +++ b/PEAR/PackageFile/v2/rw.php @@ -0,0 +1,1603 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: rw.php,v 1.20 2007/04/04 03:28:16 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a8 + */ +/** + * For base class + */ +require_once 'PEAR/PackageFile/v2.php'; +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a8 + */ +class PEAR_PackageFile_v2_rw extends PEAR_PackageFile_v2 +{ + /** + * @param string Extension name + * @return bool success of operation + */ + function setProvidesExtension($extension) + { + if (in_array($this->getPackageType(), + array('extsrc', 'extbin', 'zendextsrc', 'zendextbin'))) { + if (!isset($this->_packageInfo['providesextension'])) { + // ensure that the channel tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('usesrole', 'usestask', 'srcpackage', 'srcuri', 'phprelease', + 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), + $extension, 'providesextension'); + } + $this->_packageInfo['providesextension'] = $extension; + return true; + } + return false; + } + + function setPackage($package) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['attribs'])) { + $this->_packageInfo = array_merge(array('attribs' => array( + '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', + )), $this->_packageInfo); + } + if (!isset($this->_packageInfo['name'])) { + return $this->_packageInfo = array_merge(array('name' => $package), + $this->_packageInfo); + } + $this->_packageInfo['name'] = $package; + } + + /** + * set this as a package.xml version 2.1 + * @access private + */ + function _setPackageVersion2_1() + { + $info = array( + 'version' => '2.1', + 'xmlns' => 'http://pear.php.net/dtd/package-2.1', + '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.1 + http://pear.php.net/dtd/package-2.1.xsd', + ); + if (!isset($this->_packageInfo['attribs'])) { + $this->_packageInfo = array_merge(array('attribs' => $info), $this->_packageInfo); + } else { + $this->_packageInfo['attribs'] = $info; + } + } + + function setUri($uri) + { + unset($this->_packageInfo['channel']); + $this->_isValid = 0; + if (!isset($this->_packageInfo['uri'])) { + // ensure that the uri tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('extends', 'summary', 'description', 'lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $uri, 'uri'); + } + $this->_packageInfo['uri'] = $uri; + } + + function setChannel($channel) + { + unset($this->_packageInfo['uri']); + $this->_isValid = 0; + if (!isset($this->_packageInfo['channel'])) { + // ensure that the channel tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('extends', 'summary', 'description', 'lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $channel, 'channel'); + } + $this->_packageInfo['channel'] = $channel; + } + + function setExtends($extends) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['extends'])) { + // ensure that the extends tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('summary', 'description', 'lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $extends, 'extends'); + } + $this->_packageInfo['extends'] = $extends; + } + + function setSummary($summary) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['summary'])) { + // ensure that the summary tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('description', 'lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $summary, 'summary'); + } + $this->_packageInfo['summary'] = $summary; + } + + function setDescription($desc) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['description'])) { + // ensure that the description tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $desc, 'description'); + } + $this->_packageInfo['description'] = $desc; + } + + /** + * Adds a new maintainer - no checking of duplicates is performed, use + * updatemaintainer for that purpose. + */ + function addMaintainer($role, $handle, $name, $email, $active = 'yes') + { + if (!in_array($role, array('lead', 'developer', 'contributor', 'helper'))) { + return false; + } + if (isset($this->_packageInfo[$role])) { + if (!isset($this->_packageInfo[$role][0])) { + $this->_packageInfo[$role] = array($this->_packageInfo[$role]); + } + $this->_packageInfo[$role][] = + array( + 'name' => $name, + 'user' => $handle, + 'email' => $email, + 'active' => $active, + ); + } else { + $testarr = array('lead', + 'developer', 'contributor', 'helper', 'date', 'time', 'version', + 'stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', + 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'); + foreach (array('lead', 'developer', 'contributor', 'helper') as $testrole) { + array_shift($testarr); + if ($role == $testrole) { + break; + } + } + if (!isset($this->_packageInfo[$role])) { + // ensure that the extends tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, $testarr, + array(), $role); + } + $this->_packageInfo[$role] = + array( + 'name' => $name, + 'user' => $handle, + 'email' => $email, + 'active' => $active, + ); + } + $this->_isValid = 0; + } + + function updateMaintainer($newrole, $handle, $name, $email, $active = 'yes') + { + $found = false; + foreach (array('lead', 'developer', 'contributor', 'helper') as $role) { + if (!isset($this->_packageInfo[$role])) { + continue; + } + $info = $this->_packageInfo[$role]; + if (!isset($info[0])) { + if ($info['user'] == $handle) { + $found = true; + break; + } + } + foreach ($info as $i => $maintainer) { + if ($maintainer['user'] == $handle) { + $found = $i; + break 2; + } + } + } + if ($found === false) { + return $this->addMaintainer($newrole, $handle, $name, $email, $active); + } + if ($found !== false) { + if ($found === true) { + unset($this->_packageInfo[$role]); + } else { + unset($this->_packageInfo[$role][$found]); + $this->_packageInfo[$role] = array_values($this->_packageInfo[$role]); + } + } + $this->addMaintainer($newrole, $handle, $name, $email, $active); + $this->_isValid = 0; + } + + function deleteMaintainer($handle) + { + $found = false; + foreach (array('lead', 'developer', 'contributor', 'helper') as $role) { + if (!isset($this->_packageInfo[$role])) { + continue; + } + if (!isset($this->_packageInfo[$role][0])) { + $this->_packageInfo[$role] = array($this->_packageInfo[$role]); + } + foreach ($this->_packageInfo[$role] as $i => $maintainer) { + if ($maintainer['user'] == $handle) { + $found = $i; + break; + } + } + if ($found !== false) { + unset($this->_packageInfo[$role][$found]); + if (!count($this->_packageInfo[$role]) && $role == 'lead') { + $this->_isValid = 0; + } + if (!count($this->_packageInfo[$role])) { + unset($this->_packageInfo[$role]); + return true; + } + $this->_packageInfo[$role] = + array_values($this->_packageInfo[$role]); + if (count($this->_packageInfo[$role]) == 1) { + $this->_packageInfo[$role] = $this->_packageInfo[$role][0]; + } + return true; + } + if (count($this->_packageInfo[$role]) == 1) { + $this->_packageInfo[$role] = $this->_packageInfo[$role][0]; + } + } + return false; + } + + function setReleaseVersion($version) + { + if (isset($this->_packageInfo['version']) && + isset($this->_packageInfo['version']['release'])) { + unset($this->_packageInfo['version']['release']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $version, array( + 'version' => array('stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), + 'release' => array('api'))); + $this->_isValid = 0; + } + + function setAPIVersion($version) + { + if (isset($this->_packageInfo['version']) && + isset($this->_packageInfo['version']['api'])) { + unset($this->_packageInfo['version']['api']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $version, array( + 'version' => array('stability', 'license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), + 'api' => array())); + $this->_isValid = 0; + } + + /** + * snapshot|devel|alpha|beta|stable + */ + function setReleaseStability($state) + { + if (isset($this->_packageInfo['stability']) && + isset($this->_packageInfo['stability']['release'])) { + unset($this->_packageInfo['stability']['release']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $state, array( + 'stability' => array('license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), + 'release' => array('api'))); + $this->_isValid = 0; + } + + /** + * @param devel|alpha|beta|stable + */ + function setAPIStability($state) + { + if (isset($this->_packageInfo['stability']) && + isset($this->_packageInfo['stability']['api'])) { + unset($this->_packageInfo['stability']['api']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $state, array( + 'stability' => array('license', 'notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), + 'api' => array())); + $this->_isValid = 0; + } + + function setLicense($license, $uri = false, $filesource = false) + { + if (!isset($this->_packageInfo['license'])) { + // ensure that the license tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('notes', 'contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), 0, 'license'); + } + if ($uri || $filesource) { + $attribs = array(); + if ($uri) { + $attribs['uri'] = $uri; + } + $uri = true; // for test below + if ($filesource) { + $attribs['filesource'] = $filesource; + } + } + $license = $uri ? array('attribs' => $attribs, '_content' => $license) : $license; + $this->_packageInfo['license'] = $license; + $this->_isValid = 0; + } + + function setNotes($notes) + { + $this->_isValid = 0; + if (!isset($this->_packageInfo['notes'])) { + // ensure that the notes tag is set up in the right location + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('contents', 'compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'extbinrelease', 'bundle', 'changelog'), $notes, 'notes'); + } + $this->_packageInfo['notes'] = $notes; + } + + /** + * This is only used at install-time, after all serialization + * is over. + * @param string file name + * @param string installed path + */ + function setInstalledAs($file, $path) + { + if ($path) { + return $this->_packageInfo['filelist'][$file]['installed_as'] = $path; + } + unset($this->_packageInfo['filelist'][$file]['installed_as']); + } + + /** + * This is only used at install-time, after all serialization + * is over. + */ + function installedFile($file, $atts) + { + if (isset($this->_packageInfo['filelist'][$file])) { + $this->_packageInfo['filelist'][$file] = + array_merge($this->_packageInfo['filelist'][$file], $atts['attribs']); + } else { + $this->_packageInfo['filelist'][$file] = $atts['attribs']; + } + } + + /** + * Reset the listing of package contents + * @param string base installation dir for the whole package, if any + */ + function clearContents($baseinstall = false) + { + $this->_filesValid = false; + $this->_isValid = 0; + if (!isset($this->_packageInfo['contents'])) { + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('compatible', + 'dependencies', 'providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', + 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), array(), 'contents'); + } + if ($this->getPackageType() != 'bundle') { + $this->_packageInfo['contents'] = + array('dir' => array('attribs' => array('name' => '/'))); + if ($baseinstall) { + $this->_packageInfo['contents']['dir']['attribs']['baseinstalldir'] = $baseinstall; + } + } else { + $this->_packageInfo['contents'] = array('bundledpackage' => array()); + } + } + + /** + * @param string relative path of the bundled package. + */ + function addBundledPackage($path) + { + if ($this->getPackageType() != 'bundle') { + return false; + } + $this->_filesValid = false; + $this->_isValid = 0; + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $path, array( + 'contents' => array('compatible', 'dependencies', 'providesextension', + 'usesrole', 'usestask', 'srcpackage', 'srcuri', 'phprelease', + 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), + 'bundledpackage' => array())); + } + + /** + * @param string file name + * @param PEAR_Task_Common a read/write task + */ + function addTaskToFile($filename, $task) + { + if (!method_exists($task, 'getXml')) { + return false; + } + if (!method_exists($task, 'getName')) { + return false; + } + if (!method_exists($task, 'validate')) { + return false; + } + if (!$task->validate()) { + return false; + } + if (!isset($this->_packageInfo['contents']['dir']['file'])) { + return false; + } + $this->getTasksNs(); // discover the tasks namespace if not done already + $files = $this->_packageInfo['contents']['dir']['file']; + if (!isset($files[0])) { + $files = array($files); + $ind = false; + } else { + $ind = true; + } + foreach ($files as $i => $file) { + if (isset($file['attribs'])) { + if ($file['attribs']['name'] == $filename) { + if ($ind) { + $t = isset($this->_packageInfo['contents']['dir']['file'][$i] + ['attribs'][$this->_tasksNs . + ':' . $task->getName()]) ? + $this->_packageInfo['contents']['dir']['file'][$i] + ['attribs'][$this->_tasksNs . + ':' . $task->getName()] : false; + if ($t && !isset($t[0])) { + $this->_packageInfo['contents']['dir']['file'][$i] + [$this->_tasksNs . ':' . $task->getName()] = array($t); + } + $this->_packageInfo['contents']['dir']['file'][$i][$this->_tasksNs . + ':' . $task->getName()][] = $task->getXml(); + } else { + $t = isset($this->_packageInfo['contents']['dir']['file'] + ['attribs'][$this->_tasksNs . + ':' . $task->getName()]) ? $this->_packageInfo['contents']['dir']['file'] + ['attribs'][$this->_tasksNs . + ':' . $task->getName()] : false; + if ($t && !isset($t[0])) { + $this->_packageInfo['contents']['dir']['file'] + [$this->_tasksNs . ':' . $task->getName()] = array($t); + } + $this->_packageInfo['contents']['dir']['file'][$this->_tasksNs . + ':' . $task->getName()][] = $task->getXml(); + } + return true; + } + } + } + return false; + } + + /** + * @param string path to the file + * @param string filename + * @param array extra attributes + */ + function addFile($dir, $file, $attrs) + { + if ($this->getPackageType() == 'bundle') { + return false; + } + $this->_filesValid = false; + $this->_isValid = 0; + $dir = preg_replace(array('!\\\\+!', '!/+!'), array('/', '/'), $dir); + if ($dir == '/' || $dir == '') { + $dir = ''; + } else { + $dir .= '/'; + } + $attrs['name'] = $dir . $file; + if (!isset($this->_packageInfo['contents'])) { + // ensure that the contents tag is set up + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, + array('compatible', 'dependencies', 'providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', + 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), array(), 'contents'); + } + if (isset($this->_packageInfo['contents']['dir']['file'])) { + if (!isset($this->_packageInfo['contents']['dir']['file'][0])) { + $this->_packageInfo['contents']['dir']['file'] = + array($this->_packageInfo['contents']['dir']['file']); + } + $this->_packageInfo['contents']['dir']['file'][]['attribs'] = $attrs; + } else { + $this->_packageInfo['contents']['dir']['file']['attribs'] = $attrs; + } + } + + /** + * @param string Dependent package name + * @param string Dependent package's channel name + * @param string minimum version of specified package that this release is guaranteed to be + * compatible with + * @param string maximum version of specified package that this release is guaranteed to be + * compatible with + * @param string versions of specified package that this release is not compatible with + */ + function addCompatiblePackage($name, $channel, $min, $max, $exclude = false) + { + $this->_isValid = 0; + $set = array( + 'name' => $name, + 'channel' => $channel, + 'min' => $min, + 'max' => $max, + ); + if ($exclude) { + $set['exclude'] = $exclude; + } + $this->_isValid = 0; + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $set, array( + 'compatible' => array('dependencies', 'providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog') + )); + } + + /** + * Removes the tag entirely + */ + function resetUsesrole() + { + if (isset($this->_packageInfo['usesrole'])) { + unset($this->_packageInfo['usesrole']); + } + } + + /** + * @param string + * @param string package name or uri + * @param string channel name if non-uri + */ + function addUsesrole($role, $packageOrUri, $channel = false) { + $set = array('role' => $role); + if ($channel) { + $set['package'] = $packageOrUri; + $set['channel'] = $channel; + } else { + $set['uri'] = $packageOrUri; + } + $this->_isValid = 0; + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $set, array( + 'usesrole' => array('usestask', 'srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog') + )); + } + + /** + * Removes the tag entirely + */ + function resetUsestask() + { + if (isset($this->_packageInfo['usestask'])) { + unset($this->_packageInfo['usestask']); + } + } + + + /** + * @param string + * @param string package name or uri + * @param string channel name if non-uri + */ + function addUsestask($task, $packageOrUri, $channel = false) { + $set = array('task' => $task); + if ($channel) { + $set['package'] = $packageOrUri; + $set['channel'] = $channel; + } else { + $set['uri'] = $packageOrUri; + } + $this->_isValid = 0; + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $set, array( + 'usestask' => array('srcpackage', 'srcuri', + 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog') + )); + } + + /** + * Remove all compatible tags + */ + function clearCompatible() + { + unset($this->_packageInfo['compatible']); + } + + /** + * Reset dependencies prior to adding new ones + */ + function clearDeps() + { + if (!isset($this->_packageInfo['dependencies'])) { + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, array(), + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'))); + } + $this->_packageInfo['dependencies'] = array(); + } + + /** + * @param string minimum PHP version allowed + * @param string maximum PHP version allowed + * @param array $exclude incompatible PHP versions + */ + function setPhpDep($min, $max = false, $exclude = false) + { + $this->_isValid = 0; + $dep = + array( + 'min' => $min, + ); + if ($max) { + $dep['max'] = $max; + } + if ($exclude) { + if (count($exclude) == 1) { + $exclude = $exclude[0]; + } + $dep['exclude'] = $exclude; + } + if (isset($this->_packageInfo['dependencies']['required']['php'])) { + $this->_stack->push(__FUNCTION__, 'warning', array('dep' => + $this->_packageInfo['dependencies']['required']['php']), + 'warning: PHP dependency already exists, overwriting'); + unset($this->_packageInfo['dependencies']['required']['php']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'php' => array('pearinstaller', 'package', 'subpackage', 'extension', 'os', 'arch') + )); + return true; + } + + /** + * @param string minimum allowed PEAR installer version + * @param string maximum allowed PEAR installer version + * @param string recommended PEAR installer version + * @param array incompatible version of the PEAR installer + */ + function setPearinstallerDep($min, $max = false, $recommended = false, $exclude = false) + { + $this->_isValid = 0; + $dep = + array( + 'min' => $min, + ); + if ($max) { + $dep['max'] = $max; + } + if ($recommended) { + $dep['recommended'] = $recommended; + } + if ($exclude) { + if (count($exclude) == 1) { + $exclude = $exclude[0]; + } + $dep['exclude'] = $exclude; + } + if (isset($this->_packageInfo['dependencies']['required']['pearinstaller'])) { + $this->_stack->push(__FUNCTION__, 'warning', array('dep' => + $this->_packageInfo['dependencies']['required']['pearinstaller']), + 'warning: PEAR Installer dependency already exists, overwriting'); + unset($this->_packageInfo['dependencies']['required']['pearinstaller']); + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'pearinstaller' => array('package', 'subpackage', 'extension', 'os', 'arch') + )); + } + + /** + * Mark a package as conflicting with this package + * @param string package name + * @param string package channel + * @param string extension this package provides, if any + * @param string|false minimum version required + * @param string|false maximum version allowed + * @param array|false versions to exclude from installation + */ + function addConflictingPackageDepWithChannel($name, $channel, + $providesextension = false, $min = false, $max = false, $exclude = false) + { + $this->_isValid = 0; + $dep = $this->_constructDep($name, $channel, false, $min, $max, false, + $exclude, $providesextension, false, true); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'package' => array('subpackage', 'extension', 'os', 'arch') + )); + } + + /** + * Mark a package as conflicting with this package + * @param string package name + * @param string package channel + * @param string extension this package provides, if any + */ + function addConflictingPackageDepWithUri($name, $uri, $providesextension = false) + { + $this->_isValid = 0; + $dep = + array( + 'name' => $name, + 'uri' => $uri, + 'conflicts' => '', + ); + if ($providesextension) { + $dep['providesextension'] = $providesextension; + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'package' => array('subpackage', 'extension', 'os', 'arch') + )); + } + + function addDependencyGroup($name, $hint) + { + $this->_isValid = 0; + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, + array('attribs' => array('name' => $name, 'hint' => $hint)), + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'group' => array(), + )); + } + + /** + * @param string package name + * @param string|false channel name, false if this is a uri + * @param string|false uri name, false if this is a channel + * @param string|false minimum version required + * @param string|false maximum version allowed + * @param string|false recommended installation version + * @param array|false versions to exclude from installation + * @param string extension this package provides, if any + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + * @param bool if true, tells the installer to negate this dependency (conflicts) + * @return array + * @access private + */ + function _constructDep($name, $channel, $uri, $min, $max, $recommended, $exclude, + $providesextension = false, $nodefault = false, + $conflicts = false) + { + $dep = + array( + 'name' => $name, + ); + if ($channel) { + $dep['channel'] = $channel; + } elseif ($uri) { + $dep['uri'] = $uri; + } + if ($min) { + $dep['min'] = $min; + } + if ($max) { + $dep['max'] = $max; + } + if ($recommended) { + $dep['recommended'] = $recommended; + } + if ($exclude) { + if (is_array($exclude) && count($exclude) == 1) { + $exclude = $exclude[0]; + } + $dep['exclude'] = $exclude; + } + if ($conflicts) { + $dep['conflicts'] = ''; + } + if ($nodefault) { + $dep['nodefault'] = ''; + } + if ($providesextension) { + $dep['providesextension'] = $providesextension; + } + return $dep; + } + + /** + * @param package|subpackage + * @param string group name + * @param string package name + * @param string package channel + * @param string minimum version + * @param string maximum version + * @param string recommended version + * @param array|false optional excluded versions + * @param string extension this package provides, if any + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + * @return bool false if the dependency group has not been initialized with + * {@link addDependencyGroup()}, or a subpackage is added with + * a providesextension + */ + function addGroupPackageDepWithChannel($type, $groupname, $name, $channel, $min = false, + $max = false, $recommended = false, $exclude = false, + $providesextension = false, $nodefault = false) + { + if ($type == 'subpackage' && $providesextension) { + return false; // subpackages must be php packages + } + $dep = $this->_constructDep($name, $channel, false, $min, $max, $recommended, $exclude, + $providesextension, $nodefault); + return $this->_addGroupDependency($type, $dep, $groupname); + } + + /** + * @param package|subpackage + * @param string group name + * @param string package name + * @param string package uri + * @param string extension this package provides, if any + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + * @return bool false if the dependency group has not been initialized with + * {@link addDependencyGroup()} + */ + function addGroupPackageDepWithURI($type, $groupname, $name, $uri, $providesextension = false, + $nodefault = false) + { + if ($type == 'subpackage' && $providesextension) { + return false; // subpackages must be php packages + } + $dep = $this->_constructDep($name, false, $uri, false, false, false, false, + $providesextension, $nodefault); + return $this->_addGroupDependency($type, $dep, $groupname); + } + + /** + * @param string group name (must be pre-existing) + * @param string extension name + * @param string minimum version allowed + * @param string maximum version allowed + * @param string recommended version + * @param array incompatible versions + */ + function addGroupExtensionDep($groupname, $name, $min = false, $max = false, + $recommended = false, $exclude = false) + { + $this->_isValid = 0; + $dep = $this->_constructDep($name, false, false, $min, $max, $recommended, $exclude); + return $this->_addGroupDependency('extension', $dep, $groupname); + } + + /** + * @param package|subpackage|extension + * @param array dependency contents + * @param string name of the dependency group to add this to + * @return boolean + * @access private + */ + function _addGroupDependency($type, $dep, $groupname) + { + $arr = array('subpackage', 'extension'); + if ($type != 'package') { + array_shift($arr); + } + if ($type == 'extension') { + array_shift($arr); + } + if (!isset($this->_packageInfo['dependencies']['group'])) { + return false; + } else { + if (!isset($this->_packageInfo['dependencies']['group'][0])) { + if ($this->_packageInfo['dependencies']['group']['attribs']['name'] == $groupname) { + $this->_packageInfo['dependencies']['group'] = $this->_mergeTag( + $this->_packageInfo['dependencies']['group'], $dep, + array( + $type => $arr + )); + $this->_isValid = 0; + return true; + } else { + return false; + } + } else { + foreach ($this->_packageInfo['dependencies']['group'] as $i => $group) { + if ($group['attribs']['name'] == $groupname) { + $this->_packageInfo['dependencies']['group'][$i] = $this->_mergeTag( + $this->_packageInfo['dependencies']['group'][$i], $dep, + array( + $type => $arr + )); + $this->_isValid = 0; + return true; + } + } + return false; + } + } + } + + /** + * @param optional|required + * @param string package name + * @param string package channel + * @param string minimum version + * @param string maximum version + * @param string recommended version + * @param string extension this package provides, if any + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + * @param array|false optional excluded versions + */ + function addPackageDepWithChannel($type, $name, $channel, $min = false, $max = false, + $recommended = false, $exclude = false, + $providesextension = false, $nodefault = false) + { + if (!in_array($type, array('optional', 'required'), true)) { + $type = 'required'; + } + $this->_isValid = 0; + $arr = array('optional', 'group'); + if ($type != 'required') { + array_shift($arr); + } + $dep = $this->_constructDep($name, $channel, false, $min, $max, $recommended, $exclude, + $providesextension, $nodefault); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + $type => $arr, + 'package' => array('subpackage', 'extension', 'os', 'arch') + )); + } + + /** + * @param optional|required + * @param string name of the package + * @param string uri of the package + * @param string extension this package provides, if any + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + */ + function addPackageDepWithUri($type, $name, $uri, $providesextension = false, + $nodefault = false) + { + $this->_isValid = 0; + $arr = array('optional', 'group'); + if ($type != 'required') { + array_shift($arr); + } + $dep = $this->_constructDep($name, false, $uri, false, false, false, false, + $providesextension, $nodefault); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + $type => $arr, + 'package' => array('subpackage', 'extension', 'os', 'arch') + )); + } + + /** + * @param optional|required optional, required + * @param string package name + * @param string package channel + * @param string minimum version + * @param string maximum version + * @param string recommended version + * @param array incompatible versions + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + */ + function addSubpackageDepWithChannel($type, $name, $channel, $min = false, $max = false, + $recommended = false, $exclude = false, + $nodefault = false) + { + $this->_isValid = 0; + $arr = array('optional', 'group'); + if ($type != 'required') { + array_shift($arr); + } + $dep = $this->_constructDep($name, $channel, false, $min, $max, $recommended, $exclude, + $nodefault); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + $type => $arr, + 'subpackage' => array('extension', 'os', 'arch') + )); + } + + /** + * @param optional|required optional, required + * @param string package name + * @param string package uri for download + * @param bool if true, tells the installer to ignore the default optional dependency group + * when installing this package + */ + function addSubpackageDepWithUri($type, $name, $uri, $nodefault = false) + { + $this->_isValid = 0; + $arr = array('optional', 'group'); + if ($type != 'required') { + array_shift($arr); + } + $dep = $this->_constructDep($name, false, $uri, false, false, false, false, $nodefault); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + $type => $arr, + 'subpackage' => array('extension', 'os', 'arch') + )); + } + + /** + * @param optional|required optional, required + * @param string extension name + * @param string minimum version + * @param string maximum version + * @param string recommended version + * @param array incompatible versions + */ + function addExtensionDep($type, $name, $min = false, $max = false, $recommended = false, + $exclude = false) + { + $this->_isValid = 0; + $arr = array('optional', 'group'); + if ($type != 'required') { + array_shift($arr); + } + $dep = $this->_constructDep($name, false, false, $min, $max, $recommended, $exclude); + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + $type => $arr, + 'extension' => array('os', 'arch') + )); + } + + /** + * @param string Operating system name + * @param boolean true if this package cannot be installed on this OS + */ + function addOsDep($name, $conflicts = false) + { + $this->_isValid = 0; + $dep = array('name' => $name); + if ($conflicts) { + $dep['conflicts'] = ''; + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'os' => array('arch') + )); + } + + /** + * @param string Architecture matching pattern + * @param boolean true if this package cannot be installed on this architecture + */ + function addArchDep($pattern, $conflicts = false) + { + $this->_isValid = 0; + $dep = array('pattern' => $pattern); + if ($conflicts) { + $dep['conflicts'] = ''; + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, $dep, + array( + 'dependencies' => array('providesextension', 'usesrole', 'usestask', + 'srcpackage', 'srcuri', 'phprelease', 'extsrcrelease', 'extbinrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle', 'changelog'), + 'required' => array('optional', 'group'), + 'arch' => array() + )); + } + + /** + * Set the kind of package, and erase all release tags + * + * - a php package is a PEAR-style package + * - an extbin package is a PECL-style extension binary + * - an extsrc package is a PECL-style source for a binary + * - an zendextbin package is a PECL-style zend extension binary + * - an zendextsrc package is a PECL-style source for a zend extension binary + * - a bundle package is a collection of other pre-packaged packages + * @param php|extbin|extsrc|zendextsrc|zendextbin|bundle + * @return bool success + */ + function setPackageType($type) + { + $this->_isValid = 0; + if (!in_array($type, array('php', 'extbin', 'extsrc', 'zendextsrc', + 'zendextbin', 'bundle'))) { + return false; + } + if (in_array($type, array('zendextsrc', 'zendextbin'))) { + $this->_setPackageVersion2_1(); + } + if ($type != 'bundle') { + $type .= 'release'; + } + foreach (array('phprelease', 'extbinrelease', 'extsrcrelease', + 'zendextsrcrelease', 'zendextbinrelease', 'bundle') as $test) { + unset($this->_packageInfo[$test]); + } + if (!isset($this->_packageInfo[$type])) { + // ensure that the release tag is set up + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, array('changelog'), + array(), $type); + } + $this->_packageInfo[$type] = array(); + return true; + } + + /** + * @return bool true if package type is set up + */ + function addRelease() + { + if ($type = $this->getPackageType()) { + if ($type != 'bundle') { + $type .= 'release'; + } + $this->_packageInfo = $this->_mergeTag($this->_packageInfo, array(), + array($type => array('changelog'))); + return true; + } + return false; + } + + /** + * Get the current release tag in order to add to it + * @param bool returns only releases that have installcondition if true + * @return array|null + */ + function &_getCurrentRelease($strict = true) + { + if ($p = $this->getPackageType()) { + if ($strict) { + if ($p == 'extsrc' || $p == 'zendextsrc') { + $a = null; + return $a; + } + } + if ($p != 'bundle') { + $p .= 'release'; + } + if (isset($this->_packageInfo[$p][0])) { + return $this->_packageInfo[$p][count($this->_packageInfo[$p]) - 1]; + } else { + return $this->_packageInfo[$p]; + } + } else { + $a = null; + return $a; + } + } + + /** + * Add a file to the current release that should be installed under a different name + * @param string path to file + * @param string name the file should be installed as + */ + function addInstallAs($path, $as) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + $r = $this->_mergeTag($r, array('attribs' => array('name' => $path, 'as' => $as)), + array( + 'filelist' => array(), + 'install' => array('ignore') + )); + } + + /** + * Add a file to the current release that should be ignored + * @param string path to file + * @return bool success of operation + */ + function addIgnore($path) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + $r = $this->_mergeTag($r, array('attribs' => array('name' => $path)), + array( + 'filelist' => array(), + 'ignore' => array() + )); + } + + /** + * Add an extension binary package for this extension source code release + * + * Note that the package must be from the same channel as the extension source package + * @param string + */ + function addBinarypackage($package) + { + if ($this->getPackageType() != 'extsrc' && $this->getPackageType() != 'zendextsrc') { + return false; + } + $r = &$this->_getCurrentRelease(false); + if ($r === null) { + return false; + } + $this->_isValid = 0; + $r = $this->_mergeTag($r, $package, + array( + 'binarypackage' => array('filelist'), + )); + } + + /** + * Add a configureoption to an extension source package + * @param string + * @param string + * @param string + */ + function addConfigureOption($name, $prompt, $default = null) + { + if ($this->getPackageType() != 'extsrc' && $this->getPackageType() != 'zendextsrc') { + return false; + } + $r = &$this->_getCurrentRelease(false); + if ($r === null) { + return false; + } + $opt = array('attribs' => array('name' => $name, 'prompt' => $prompt)); + if ($default !== null) { + $opt['default'] = $default; + } + $this->_isValid = 0; + $r = $this->_mergeTag($r, $opt, + array( + 'configureoption' => array('binarypackage', 'filelist'), + )); + } + + /** + * Set an installation condition based on php version for the current release set + * @param string minimum version + * @param string maximum version + * @param false|array incompatible versions of PHP + */ + function setPhpInstallCondition($min, $max, $exclude = false) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + if (isset($r['installconditions']['php'])) { + unset($r['installconditions']['php']); + } + $dep = array('min' => $min, 'max' => $max); + if ($exclude) { + if (is_array($exclude) && count($exclude) == 1) { + $exclude = $exclude[0]; + } + $dep['exclude'] = $exclude; + } + if ($this->getPackageType() == 'extsrc' || $this->getPackageType() == 'zendextsrc') { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('configureoption', 'binarypackage', + 'filelist'), + 'php' => array('extension', 'os', 'arch') + )); + } else { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('filelist'), + 'php' => array('extension', 'os', 'arch') + )); + } + } + + /** + * @param optional|required optional, required + * @param string extension name + * @param string minimum version + * @param string maximum version + * @param string recommended version + * @param array incompatible versions + */ + function addExtensionInstallCondition($name, $min = false, $max = false, $recommended = false, + $exclude = false) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + $dep = $this->_constructDep($name, false, false, $min, $max, $recommended, $exclude); + if ($this->getPackageType() == 'extsrc' || $this->getPackageType() == 'zendextsrc') { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('configureoption', 'binarypackage', + 'filelist'), + 'extension' => array('os', 'arch') + )); + } else { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('filelist'), + 'extension' => array('os', 'arch') + )); + } + } + + /** + * Set an installation condition based on operating system for the current release set + * @param string OS name + * @param bool whether this OS is incompatible with the current release + */ + function setOsInstallCondition($name, $conflicts = false) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + if (isset($r['installconditions']['os'])) { + unset($r['installconditions']['os']); + } + $dep = array('name' => $name); + if ($conflicts) { + $dep['conflicts'] = ''; + } + if ($this->getPackageType() == 'extsrc' || $this->getPackageType() == 'zendextsrc') { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('configureoption', 'binarypackage', + 'filelist'), + 'os' => array('arch') + )); + } else { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('filelist'), + 'os' => array('arch') + )); + } + } + + /** + * Set an installation condition based on architecture for the current release set + * @param string architecture pattern + * @param bool whether this arch is incompatible with the current release + */ + function setArchInstallCondition($pattern, $conflicts = false) + { + $r = &$this->_getCurrentRelease(); + if ($r === null) { + return false; + } + $this->_isValid = 0; + if (isset($r['installconditions']['arch'])) { + unset($r['installconditions']['arch']); + } + $dep = array('pattern' => $pattern); + if ($conflicts) { + $dep['conflicts'] = ''; + } + if ($this->getPackageType() == 'extsrc' || $this->getPackageType() == 'zendextsrc') { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('configureoption', 'binarypackage', + 'filelist'), + 'arch' => array() + )); + } else { + $r = $this->_mergeTag($r, $dep, + array( + 'installconditions' => array('filelist'), + 'arch' => array() + )); + } + } + + /** + * For extension binary releases, this is used to specify either the + * static URI to a source package, or the package name and channel of the extsrc/zendextsrc + * package it is based on. + * @param string Package name, or full URI to source package (extsrc/zendextsrc type) + */ + function setSourcePackage($packageOrUri) + { + $this->_isValid = 0; + if (isset($this->_packageInfo['channel'])) { + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, array('phprelease', + 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), + $packageOrUri, 'srcpackage'); + } else { + $this->_packageInfo = $this->_insertBefore($this->_packageInfo, array('phprelease', + 'extsrcrelease', 'extbinrelease', 'zendextsrcrelease', 'zendextbinrelease', + 'bundle', 'changelog'), $packageOrUri, 'srcuri'); + } + } + + /** + * Generate a valid change log entry from the current package.xml + * @param string|false + */ + function generateChangeLogEntry($notes = false) + { + return array( + 'version' => + array( + 'release' => $this->getVersion('release'), + 'api' => $this->getVersion('api'), + ), + 'stability' => + $this->getStability(), + 'date' => $this->getDate(), + 'license' => $this->getLicense(true), + 'notes' => $notes ? $notes : $this->getNotes() + ); + } + + /** + * @param string release version to set change log notes for + * @param array output of {@link generateChangeLogEntry()} + */ + function setChangelogEntry($releaseversion, $contents) + { + if (!isset($this->_packageInfo['changelog'])) { + $this->_packageInfo['changelog']['release'] = $contents; + return; + } + if (!isset($this->_packageInfo['changelog']['release'][0])) { + if ($this->_packageInfo['changelog']['release']['version']['release'] == $releaseversion) { + $this->_packageInfo['changelog']['release'] = array( + $this->_packageInfo['changelog']['release']); + } else { + $this->_packageInfo['changelog']['release'] = array( + $this->_packageInfo['changelog']['release']); + return $this->_packageInfo['changelog']['release'][] = $contents; + } + } + foreach($this->_packageInfo['changelog']['release'] as $index => $changelog) { + if (isset($changelog['version']) && + strnatcasecmp($changelog['version']['release'], $releaseversion) == 0) { + $curlog = $index; + } + } + if (isset($curlog)) { + $this->_packageInfo['changelog']['release'][$curlog] = $contents; + } else { + $this->_packageInfo['changelog']['release'][] = $contents; + } + } + + /** + * Remove the changelog entirely + */ + function clearChangeLog() + { + unset($this->_packageInfo['changelog']); + } +} +?> \ No newline at end of file diff --git a/PEAR/Packager.php b/PEAR/Packager.php new file mode 100644 index 0000000..fc69e99 --- /dev/null +++ b/PEAR/Packager.php @@ -0,0 +1,199 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Packager.php,v 1.70 2006/09/25 05:12:21 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR/Common.php'; +require_once 'PEAR/PackageFile.php'; +require_once 'System.php'; + +/** + * Administration class used to make a PEAR release tarball. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Packager extends PEAR_Common +{ + /** + * @var PEAR_Registry + */ + var $_registry; + // {{{ package() + + function package($pkgfile = null, $compress = true, $pkg2 = null) + { + // {{{ validate supplied package.xml file + if (empty($pkgfile)) { + $pkgfile = 'package.xml'; + } + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pkg = &new PEAR_PackageFile($this->config, $this->debug); + $pf = &$pkg->fromPackageFile($pkgfile, PEAR_VALIDATE_NORMAL); + $main = &$pf; + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pf)) { + if (is_array($pf->getUserInfo())) { + foreach ($pf->getUserInfo() as $error) { + $this->log(0, 'Error: ' . $error['message']); + } + } + $this->log(0, $pf->getMessage()); + return $this->raiseError("Cannot package, errors in package file"); + } else { + foreach ($pf->getValidationWarnings() as $warning) { + $this->log(1, 'Warning: ' . $warning['message']); + } + } + + // }}} + if ($pkg2) { + $this->log(0, 'Attempting to process the second package file'); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $pf2 = &$pkg->fromPackageFile($pkg2, PEAR_VALIDATE_NORMAL); + PEAR::staticPopErrorHandling(); + if (PEAR::isError($pf2)) { + if (is_array($pf2->getUserInfo())) { + foreach ($pf2->getUserInfo() as $error) { + $this->log(0, 'Error: ' . $error['message']); + } + } + $this->log(0, $pf2->getMessage()); + return $this->raiseError("Cannot package, errors in second package file"); + } else { + foreach ($pf2->getValidationWarnings() as $warning) { + $this->log(1, 'Warning: ' . $warning['message']); + } + } + if ($pf2->getPackagexmlVersion() == '2.0' || + $pf2->getPackagexmlVersion() == '2.1') { + $main = &$pf2; + $other = &$pf; + } else { + $main = &$pf; + $other = &$pf2; + } + if ($main->getPackagexmlVersion() != '2.0' && + $main->getPackagexmlVersion() != '2.1') { + return PEAR::raiseError('Error: cannot package two package.xml version 1.0, can ' . + 'only package together a package.xml 1.0 and package.xml 2.0'); + } + if ($other->getPackagexmlVersion() != '1.0') { + return PEAR::raiseError('Error: cannot package two package.xml version 2.0, can ' . + 'only package together a package.xml 1.0 and package.xml 2.0'); + } + } + $main->setLogger($this); + if (!$main->validate(PEAR_VALIDATE_PACKAGING)) { + foreach ($main->getValidationWarnings() as $warning) { + $this->log(0, 'Error: ' . $warning['message']); + } + return $this->raiseError("Cannot package, errors in package"); + } else { + foreach ($main->getValidationWarnings() as $warning) { + $this->log(1, 'Warning: ' . $warning['message']); + } + } + if ($pkg2) { + $other->setLogger($this); + $a = false; + if (!$other->validate(PEAR_VALIDATE_NORMAL) || $a = !$main->isEquivalent($other)) { + foreach ($other->getValidationWarnings() as $warning) { + $this->log(0, 'Error: ' . $warning['message']); + } + foreach ($main->getValidationWarnings() as $warning) { + $this->log(0, 'Error: ' . $warning['message']); + } + if ($a) { + return $this->raiseError('The two package.xml files are not equivalent!'); + } + return $this->raiseError("Cannot package, errors in package"); + } else { + foreach ($other->getValidationWarnings() as $warning) { + $this->log(1, 'Warning: ' . $warning['message']); + } + } + $gen = &$main->getDefaultGenerator(); + $tgzfile = $gen->toTgz2($this, $other, $compress); + if (PEAR::isError($tgzfile)) { + return $tgzfile; + } + $dest_package = basename($tgzfile); + $pkgdir = dirname($pkgfile); + + // TAR the Package ------------------------------------------- + $this->log(1, "Package $dest_package done"); + if (file_exists("$pkgdir/CVS/Root")) { + $cvsversion = preg_replace('/[^a-z0-9]/i', '_', $pf->getVersion()); + $cvstag = "RELEASE_$cvsversion"; + $this->log(1, 'Tag the released code with "pear cvstag ' . + $main->getPackageFile() . '"'); + $this->log(1, "(or set the CVS tag $cvstag by hand)"); + } + } else { // this branch is executed for single packagefile packaging + $gen = &$pf->getDefaultGenerator(); + $tgzfile = $gen->toTgz($this, $compress); + if (PEAR::isError($tgzfile)) { + $this->log(0, $tgzfile->getMessage()); + return $this->raiseError("Cannot package, errors in package"); + } + $dest_package = basename($tgzfile); + $pkgdir = dirname($pkgfile); + + // TAR the Package ------------------------------------------- + $this->log(1, "Package $dest_package done"); + if (file_exists("$pkgdir/CVS/Root")) { + $cvsversion = preg_replace('/[^a-z0-9]/i', '_', $pf->getVersion()); + $cvstag = "RELEASE_$cvsversion"; + $this->log(1, "Tag the released code with `pear cvstag $pkgfile'"); + $this->log(1, "(or set the CVS tag $cvstag by hand)"); + } + } + return $dest_package; + } + + // }}} +} + +// {{{ md5_file() utility function +if (!function_exists('md5_file')) { + function md5_file($file) { + if (!$fd = @fopen($file, 'r')) { + return false; + } + fclose($fd); + $md5 = md5(file_get_contents($file)); + return $md5; + } +} +// }}} + +?> diff --git a/PEAR/REST.php b/PEAR/REST.php new file mode 100644 index 0000000..01c1e2c --- /dev/null +++ b/PEAR/REST.php @@ -0,0 +1,395 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: REST.php,v 1.22 2007/06/10 04:16:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * For downloading xml files + */ +require_once 'PEAR.php'; +require_once 'PEAR/XMLParser.php'; + +/** + * Intelligently retrieve data, following hyperlinks if necessary, and re-directing + * as well + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_REST +{ + var $config; + var $_options; + function PEAR_REST(&$config, $options = array()) + { + $this->config = &$config; + $this->_options = $options; + } + + /** + * Retrieve REST data, but always retrieve the local cache if it is available. + * + * This is useful for elements that should never change, such as information on a particular + * release + * @param string full URL to this resource + * @param array|false contents of the accept-encoding header + * @param boolean if true, xml will be returned as a string, otherwise, xml will be + * parsed using PEAR_XMLParser + * @return string|array + */ + function retrieveCacheFirst($url, $accept = false, $forcestring = false) + { + $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cachefile'; + if (file_exists($cachefile)) { + return unserialize(implode('', file($cachefile))); + } + return $this->retrieveData($url, $accept, $forcestring); + } + + /** + * Retrieve a remote REST resource + * @param string full URL to this resource + * @param array|false contents of the accept-encoding header + * @param boolean if true, xml will be returned as a string, otherwise, xml will be + * parsed using PEAR_XMLParser + * @return string|array + */ + function retrieveData($url, $accept = false, $forcestring = false) + { + $cacheId = $this->getCacheId($url); + if ($ret = $this->useLocalCache($url, $cacheId)) { + return $ret; + } + if (!isset($this->_options['offline'])) { + $trieddownload = true; + $file = $this->downloadHttp($url, $cacheId ? $cacheId['lastChange'] : false, $accept); + } else { + $trieddownload = false; + $file = false; + } + if (PEAR::isError($file)) { + if ($file->getCode() == -9276) { + $trieddownload = false; + $file = false; // use local copy if available on socket connect error + } else { + return $file; + } + } + if (!$file) { + $ret = $this->getCache($url); + if (!PEAR::isError($ret) && $trieddownload) { + // reset the age of the cache if the server says it was unmodified + $this->saveCache($url, $ret, null, true, $cacheId); + } + return $ret; + } + if (is_array($file)) { + $headers = $file[2]; + $lastmodified = $file[1]; + $content = $file[0]; + } else { + $content = $file; + $lastmodified = false; + $headers = array(); + } + if ($forcestring) { + $this->saveCache($url, $content, $lastmodified, false, $cacheId); + return $content; + } + if (isset($headers['content-type'])) { + switch ($headers['content-type']) { + case 'text/xml' : + case 'application/xml' : + $parser = new PEAR_XMLParser; + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $err = $parser->parse($content); + PEAR::popErrorHandling(); + if (PEAR::isError($err)) { + return PEAR::raiseError('Invalid xml downloaded from "' . $url . '": ' . + $err->getMessage()); + } + $content = $parser->getData(); + case 'text/html' : + default : + // use it as a string + } + } else { + // assume XML + $parser = new PEAR_XMLParser; + $parser->parse($content); + $content = $parser->getData(); + } + $this->saveCache($url, $content, $lastmodified, false, $cacheId); + return $content; + } + + function useLocalCache($url, $cacheid = null) + { + if ($cacheid === null) { + $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cacheid'; + if (file_exists($cacheidfile)) { + $cacheid = unserialize(implode('', file($cacheidfile))); + } else { + return false; + } + } + $cachettl = $this->config->get('cache_ttl'); + // If cache is newer than $cachettl seconds, we use the cache! + if (time() - $cacheid['age'] < $cachettl) { + return $this->getCache($url); + } + return false; + } + + function getCacheId($url) + { + $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cacheid'; + if (file_exists($cacheidfile)) { + $ret = unserialize(implode('', file($cacheidfile))); + return $ret; + } else { + return false; + } + } + + function getCache($url) + { + $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cachefile'; + if (file_exists($cachefile)) { + return unserialize(implode('', file($cachefile))); + } else { + return PEAR::raiseError('No cached content available for "' . $url . '"'); + } + } + + /** + * @param string full URL to REST resource + * @param string original contents of the REST resource + * @param array HTTP Last-Modified and ETag headers + * @param bool if true, then the cache id file should be regenerated to + * trigger a new time-to-live value + */ + function saveCache($url, $contents, $lastmodified, $nochange = false, $cacheid = null) + { + $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cacheid'; + $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cachefile'; + if ($cacheid === null && $nochange) { + $cacheid = unserialize(implode('', file($cacheidfile))); + } + + $fp = @fopen($cacheidfile, 'wb'); + if (!$fp) { + $cache_dir = $this->config->get('cache_dir'); + if (!is_dir($cache_dir)) { + System::mkdir(array('-p', $cache_dir)); + $fp = @fopen($cacheidfile, 'wb'); + if (!$fp) { + return false; + } + } else { + return false; + } + } + + if ($nochange) { + fwrite($fp, serialize(array( + 'age' => time(), + 'lastChange' => $cacheid['lastChange'], + ))); + fclose($fp); + return true; + } else { + fwrite($fp, serialize(array( + 'age' => time(), + 'lastChange' => $lastmodified, + ))); + } + fclose($fp); + $fp = @fopen($cachefile, 'wb'); + if (!$fp) { + if (file_exists($cacheidfile)) { + @unlink($cacheidfile); + } + return false; + } + fwrite($fp, serialize($contents)); + fclose($fp); + return true; + } + + /** + * Efficiently Download a file through HTTP. Returns downloaded file as a string in-memory + * This is best used for small files + * + * If an HTTP proxy has been configured (http_proxy PEAR_Config + * setting), the proxy will be used. + * + * @param string $url the URL to download + * @param string $save_dir directory to save file in + * @param false|string|array $lastmodified header values to check against for caching + * use false to return the header values from this download + * @param false|array $accept Accept headers to send + * @return string|array Returns the contents of the downloaded file or a PEAR + * error on failure. If the error is caused by + * socket-related errors, the error object will + * have the fsockopen error code available through + * getCode(). If caching is requested, then return the header + * values. + * + * @access public + */ + function downloadHttp($url, $lastmodified = null, $accept = false) + { + $info = parse_url($url); + if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) { + return PEAR::raiseError('Cannot download non-http URL "' . $url . '"'); + } + if (!isset($info['host'])) { + return PEAR::raiseError('Cannot download from non-URL "' . $url . '"'); + } else { + $host = $info['host']; + if (!array_key_exists('port', $info)) { + $info['port'] = null; + } + if (!array_key_exists('path', $info)) { + $info['path'] = null; + } + $port = $info['port']; + $path = $info['path']; + } + $proxy_host = $proxy_port = $proxy_user = $proxy_pass = ''; + if ($this->config->get('http_proxy')&& + $proxy = parse_url($this->config->get('http_proxy'))) { + $proxy_host = isset($proxy['host']) ? $proxy['host'] : null; + if (isset($proxy['scheme']) && $proxy['scheme'] == 'https') { + $proxy_host = 'ssl://' . $proxy_host; + } + $proxy_port = isset($proxy['port']) ? $proxy['port'] : 8080; + $proxy_user = isset($proxy['user']) ? urldecode($proxy['user']) : null; + $proxy_pass = isset($proxy['pass']) ? urldecode($proxy['pass']) : null; + } + if (empty($port)) { + if (isset($info['scheme']) && $info['scheme'] == 'https') { + $port = 443; + } else { + $port = 80; + } + } + If (isset($proxy['host'])) { + $request = "GET $url HTTP/1.1\r\n"; + } else { + $request = "GET $path HTTP/1.1\r\n"; + } + + $ifmodifiedsince = ''; + if (is_array($lastmodified)) { + if (isset($lastmodified['Last-Modified'])) { + $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n"; + } + if (isset($lastmodified['ETag'])) { + $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n"; + } + } else { + $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : ''); + } + $request .= "Host: $host:$port\r\n" . $ifmodifiedsince . + "User-Agent: PEAR/1.6.1/PHP/" . PHP_VERSION . "\r\n"; + $username = $this->config->get('username'); + $password = $this->config->get('password'); + if ($username && $password) { + $tmp = base64_encode("$username:$password"); + $request .= "Authorization: Basic $tmp\r\n"; + } + if ($proxy_host != '' && $proxy_user != '') { + $request .= 'Proxy-Authorization: Basic ' . + base64_encode($proxy_user . ':' . $proxy_pass) . "\r\n"; + } + if ($accept) { + $request .= 'Accept: ' . implode(', ', $accept) . "\r\n"; + } + $request .= "Connection: close\r\n"; + $request .= "\r\n"; + if ($proxy_host != '') { + $fp = @fsockopen($proxy_host, $proxy_port, $errno, $errstr, 15); + if (!$fp) { + return PEAR::raiseError("Connection to `$proxy_host:$proxy_port' failed: $errstr", + -9276); + } + } else { + if (isset($info['scheme']) && $info['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + $fp = @fsockopen($host, $port, $errno, $errstr); + if (!$fp) { + return PEAR::raiseError("Connection to `$host:$port' failed: $errstr", $errno); + } + } + fwrite($fp, $request); + $headers = array(); + while (trim($line = fgets($fp, 1024))) { + if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) { + $headers[strtolower($matches[1])] = trim($matches[2]); + } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) { + if ($matches[1] == 304 && ($lastmodified || ($lastmodified === false))) { + return false; + } + if ($matches[1] != 200) { + return PEAR::raiseError("File http://$host:$port$path not valid (received: $line)", (int) $matches[1]); + } + } + } + if (isset($headers['content-length'])) { + $length = $headers['content-length']; + } else { + $length = -1; + } + $data = ''; + while ($chunk = @fread($fp, 8192)) { + $data .= $chunk; + } + fclose($fp); + if ($lastmodified === false || $lastmodified) { + if (isset($headers['etag'])) { + $lastmodified = array('ETag' => $headers['etag']); + } + if (isset($headers['last-modified'])) { + if (is_array($lastmodified)) { + $lastmodified['Last-Modified'] = $headers['last-modified']; + } else { + $lastmodified = $headers['last-modified']; + } + } + return array($data, $lastmodified, $headers); + } + return $data; + } +} +?> diff --git a/PEAR/REST/10.php b/PEAR/REST/10.php new file mode 100644 index 0000000..a1ccdff --- /dev/null +++ b/PEAR/REST/10.php @@ -0,0 +1,812 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: 10.php,v 1.48 2007/06/01 23:41:34 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a12 + */ + +/** + * For downloading REST xml/txt files + */ +require_once 'PEAR/REST.php'; + +/** + * Implement REST 1.0 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a12 + */ +class PEAR_REST_10 +{ + /** + * @var PEAR_REST + */ + var $_rest; + function PEAR_REST_10($config, $options = array()) + { + $this->_rest = &new PEAR_REST($config, $options); + } + + /** + * Retrieve information about a remote package to be downloaded from a REST server + * + * @param string $base The uri to prepend to all REST calls + * @param array $packageinfo an array of format: + *
+     *  array(
+     *   'package' => 'packagename',
+     *   'channel' => 'channelname',
+     *  ['state' => 'alpha' (or valid state),]
+     *  -or-
+     *  ['version' => '1.whatever']
+     * 
+ * @param string $prefstate Current preferred_state config variable value + * @param bool $installed the installed version of this package to compare against + * @return array|false|PEAR_Error see {@link _returnDownloadURL()} + */ + function getDownloadURL($base, $packageinfo, $prefstate, $installed) + { + $channel = $packageinfo['channel']; + $package = $packageinfo['package']; + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + $state = $version = null; + if (isset($packageinfo['state'])) { + $state = $packageinfo['state']; + } + if (isset($packageinfo['version'])) { + $version = $packageinfo['version']; + } + $info = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . '/allreleases.xml'); + if (PEAR::isError($info)) { + return PEAR::raiseError('No releases available for package "' . + $channel . '/' . $package . '"'); + } + if (!isset($info['r'])) { + return false; + } + $found = false; + $release = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + foreach ($info['r'] as $release) { + if (!isset($this->_rest->_options['force']) && ($installed && + version_compare($release['v'], $installed, '<'))) { + continue; + } + if (isset($state)) { + // try our preferred state first + if ($release['s'] == $state) { + $found = true; + break; + } + // see if there is something newer and more stable + // bug #7221 + if (in_array($release['s'], $this->betterStates($state), true)) { + $found = true; + break; + } + } elseif (isset($version)) { + if ($release['v'] == $version) { + $found = true; + break; + } + } else { + if (in_array($release['s'], $states)) { + $found = true; + break; + } + } + } + return $this->_returnDownloadURL($base, $package, $release, $info, $found); + } + + function getDepDownloadURL($base, $xsdversion, $dependency, $deppackage, + $prefstate = 'stable', $installed = false) + { + $channel = $dependency['channel']; + $package = $dependency['name']; + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + $state = $version = null; + if (isset($packageinfo['state'])) { + $state = $packageinfo['state']; + } + if (isset($packageinfo['version'])) { + $version = $packageinfo['version']; + } + $info = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . '/allreleases.xml'); + if (PEAR::isError($info)) { + return PEAR::raiseError('Package "' . $deppackage['channel'] . '/' . $deppackage['package'] + . '" dependency "' . $channel . '/' . $package . '" has no releases'); + } + if (!is_array($info) || !isset($info['r'])) { + return false; + } + $exclude = array(); + $min = $max = $recommended = false; + if ($xsdversion == '1.0') { + $pinfo['package'] = $dependency['name']; + $pinfo['channel'] = 'pear.php.net'; // this is always true - don't change this + switch ($dependency['rel']) { + case 'ge' : + $min = $dependency['version']; + break; + case 'gt' : + $min = $dependency['version']; + $exclude = array($dependency['version']); + break; + case 'eq' : + $recommended = $dependency['version']; + break; + case 'lt' : + $max = $dependency['version']; + $exclude = array($dependency['version']); + break; + case 'le' : + $max = $dependency['version']; + break; + case 'ne' : + $exclude = array($dependency['version']); + break; + } + } else { + $pinfo['package'] = $dependency['name']; + $min = isset($dependency['min']) ? $dependency['min'] : false; + $max = isset($dependency['max']) ? $dependency['max'] : false; + $recommended = isset($dependency['recommended']) ? + $dependency['recommended'] : false; + if (isset($dependency['exclude'])) { + if (!isset($dependency['exclude'][0])) { + $exclude = array($dependency['exclude']); + } + } + } + $found = false; + $release = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + foreach ($info['r'] as $release) { + if (!isset($this->_rest->_options['force']) && ($installed && + version_compare($release['v'], $installed, '<'))) { + continue; + } + if (in_array($release['v'], $exclude)) { // skip excluded versions + continue; + } + // allow newer releases to say "I'm OK with the dependent package" + if ($xsdversion == '2.0' && isset($release['co'])) { + if (!is_array($release['co']) || !isset($release['co'][0])) { + $release['co'] = array($release['co']); + } + foreach ($release['co'] as $entry) { + if (isset($entry['x']) && !is_array($entry['x'])) { + $entry['x'] = array($entry['x']); + } elseif (!isset($entry['x'])) { + $entry['x'] = array(); + } + if ($entry['c'] == $deppackage['channel'] && + strtolower($entry['p']) == strtolower($deppackage['package']) && + version_compare($deppackage['version'], $entry['min'], '>=') && + version_compare($deppackage['version'], $entry['max'], '<=') && + !in_array($release['v'], $entry['x'])) { + $recommended = $release['v']; + break; + } + } + } + if ($recommended) { + if ($release['v'] != $recommended) { // if we want a specific + // version, then skip all others + continue; + } else { + if (!in_array($release['s'], $states)) { + // the stability is too low, but we must return the + // recommended version if possible + return $this->_returnDownloadURL($base, $package, $release, $info, true); + } + } + } + if ($min && version_compare($release['v'], $min, 'lt')) { // skip too old versions + continue; + } + if ($max && version_compare($release['v'], $max, 'gt')) { // skip too new versions + continue; + } + if ($installed && version_compare($release['v'], $installed, '<')) { + continue; + } + if (in_array($release['s'], $states)) { // if in the preferred state... + $found = true; // ... then use it + break; + } + } + return $this->_returnDownloadURL($base, $package, $release, $info, $found); + } + + /** + * Take raw data and return the array needed for processing a download URL + * + * @param string $base REST base uri + * @param string $package Package name + * @param array $release an array of format array('v' => version, 's' => state) + * describing the release to download + * @param array $info list of all releases as defined by allreleases.xml + * @param bool|null $found determines whether the release was found or this is the next + * best alternative. If null, then versions were skipped because + * of PHP dependency + * @return array|PEAR_Error + * @access private + */ + function _returnDownloadURL($base, $package, $release, $info, $found, $phpversion = false) + { + if (!$found) { + $release = $info['r'][0]; + } + $pinfo = $this->_rest->retrieveCacheFirst($base . 'p/' . strtolower($package) . '/' . + 'info.xml'); + if (PEAR::isError($pinfo)) { + return PEAR::raiseError('Package "' . $package . + '" does not have REST info xml available'); + } + $releaseinfo = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) . '/' . + $release['v'] . '.xml'); + if (PEAR::isError($releaseinfo)) { + return PEAR::raiseError('Package "' . $package . '" Version "' . $release['v'] . + '" does not have REST xml available'); + } + $packagexml = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) . '/' . + 'deps.' . $release['v'] . '.txt', false, true); + if (PEAR::isError($packagexml)) { + return PEAR::raiseError('Package "' . $package . '" Version "' . $release['v'] . + '" does not have REST dependency information available'); + } + $packagexml = unserialize($packagexml); + if (!$packagexml) { + $packagexml = array(); + } + $allinfo = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases.xml'); + if (!is_array($allinfo['r']) || !isset($allinfo['r'][0])) { + $allinfo['r'] = array($allinfo['r']); + } + $compatible = false; + foreach ($allinfo['r'] as $release) { + if ($release['v'] != $releaseinfo['v']) { + continue; + } + if (!isset($release['co'])) { + break; + } + $compatible = array(); + if (!is_array($release['co']) || !isset($release['co'][0])) { + $release['co'] = array($release['co']); + } + foreach ($release['co'] as $entry) { + $comp = array(); + $comp['name'] = $entry['p']; + $comp['channel'] = $entry['c']; + $comp['min'] = $entry['min']; + $comp['max'] = $entry['max']; + if (isset($entry['x']) && !is_array($entry['x'])) { + $comp['exclude'] = $entry['x']; + } + $compatible[] = $comp; + } + if (count($compatible) == 1) { + $compatible = $compatible[0]; + } + break; + } + if (isset($pinfo['dc']) && isset($pinfo['dp'])) { + if (is_array($pinfo['dp'])) { + $deprecated = array('channel' => (string) $pinfo['dc'], + 'package' => trim($pinfo['dp']['_content'])); + } else { + $deprecated = array('channel' => (string) $pinfo['dc'], + 'package' => trim($pinfo['dp'])); + } + } else { + $deprecated = false; + } + if ($found) { + return + array('version' => $releaseinfo['v'], + 'info' => $packagexml, + 'package' => $releaseinfo['p']['_content'], + 'stability' => $releaseinfo['st'], + 'url' => $releaseinfo['g'], + 'compatible' => $compatible, + 'deprecated' => $deprecated, + ); + } else { + return + array('version' => $releaseinfo['v'], + 'package' => $releaseinfo['p']['_content'], + 'stability' => $releaseinfo['st'], + 'info' => $packagexml, + 'compatible' => $compatible, + 'deprecated' => $deprecated, + 'php' => $phpversion + ); + } + } + + function listPackages($base) + { + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml'); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if (!is_array($packagelist) || !isset($packagelist['p'])) { + return array(); + } + if (!is_array($packagelist['p'])) { + $packagelist['p'] = array($packagelist['p']); + } + return $packagelist['p']; + } + + /** + * List all categories of a REST server + * + * @param string $base base URL of the server + * @return array of categorynames + */ + function listCategories($base) + { + $categories = array(); + + // c/categories.xml does not exist; + // check for every package its category manually + // This is SLOOOWWWW : /// + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml'); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if (!is_array($packagelist) || !isset($packagelist['p'])) { + $ret = array(); + return $ret; + } + if (!is_array($packagelist['p'])) { + $packagelist['p'] = array($packagelist['p']); + } + + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + foreach ($packagelist['p'] as $package) { + $inf = $this->_rest->retrieveData($base . 'p/' . strtolower($package) . '/info.xml'); + if (PEAR::isError($inf)) { + PEAR::popErrorHandling(); + return $inf; + } + $cat = $inf['ca']['_content']; + if (!isset($categories[$cat])) { + $categories[$cat] = $inf['ca']; + } + } + return array_values($categories); + } + + /** + * List a category of a REST server + * + * @param string $base base URL of the server + * @param string $category name of the category + * @param boolean $info also download full package info + * @return array of packagenames + */ + function listCategory($base, $category, $info=false) + { + // gives '404 Not Found' error when category doesn't exist + $packagelist = $this->_rest->retrieveData($base.'c/'.urlencode($category).'/packages.xml'); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if (!is_array($packagelist) || !isset($packagelist['p'])) { + return array(); + } + if (!is_array($packagelist['p']) || + !isset($packagelist['p'][0])) { // only 1 pkg + $packagelist = array($packagelist['p']); + } else { + $packagelist = $packagelist['p']; + } + + if ($info == true) { + // get individual package info + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + foreach ($packagelist as $i => $packageitem) { + $url = sprintf('%s'.'r/%s/latest.txt', + $base, + strtolower($packageitem['_content'])); + $version = $this->_rest->retrieveData($url); + if (PEAR::isError($version)) { + break; // skipit + } + $url = sprintf('%s'.'r/%s/%s.xml', + $base, + strtolower($packageitem['_content']), + $version); + $info = $this->_rest->retrieveData($url); + if (PEAR::isError($info)) { + break; // skipit + } + $packagelist[$i]['info'] = $info; + } + PEAR::popErrorHandling(); + } + + return $packagelist; + } + + + function listAll($base, $dostable, $basic = true, $searchpackage = false, $searchsummary = false) + { + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml'); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if ($this->_rest->config->get('verbose') > 0) { + $ui = &PEAR_Frontend::singleton(); + $ui->log('Retrieving data...0%', false); + } + $ret = array(); + if (!is_array($packagelist) || !isset($packagelist['p'])) { + return $ret; + } + if (!is_array($packagelist['p'])) { + $packagelist['p'] = array($packagelist['p']); + } + + // only search-packagename = quicksearch ! + if ($searchpackage && (!$searchsummary || empty($searchpackage))) { + $newpackagelist = array(); + foreach ($packagelist['p'] as $package) { + if (!empty($searchpackage) && stristr($package, $searchpackage) !== false) { + $newpackagelist[] = $package; + } + } + $packagelist['p'] = $newpackagelist; + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $next = .1; + foreach ($packagelist['p'] as $progress => $package) { + if ($this->_rest->config->get('verbose') > 0) { + if ($progress / count($packagelist['p']) >= $next) { + if ($next == .5) { + $ui->log('50%', false); + } else { + $ui->log('.', false); + } + $next += .1; + } + } + if ($basic) { // remote-list command + if ($dostable) { + $latest = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/stable.txt'); + } else { + $latest = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/latest.txt'); + } + if (PEAR::isError($latest)) { + $latest = false; + } + $info = array('stable' => $latest); + } else { // list-all command + $inf = $this->_rest->retrieveData($base . 'p/' . strtolower($package) . '/info.xml'); + if (PEAR::isError($inf)) { + PEAR::popErrorHandling(); + return $inf; + } + if ($searchpackage) { + $found = (!empty($searchpackage) && stristr($package, $searchpackage) !== false); + if (!$found && !(isset($searchsummary) && !empty($searchsummary) + && (stristr($inf['s'], $searchsummary) !== false + || stristr($inf['d'], $searchsummary) !== false))) + { + continue; + }; + } + $releases = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases.xml'); + if (PEAR::isError($releases)) { + continue; + } + if (!isset($releases['r'][0])) { + $releases['r'] = array($releases['r']); + } + unset($latest); + unset($unstable); + unset($stable); + unset($state); + foreach ($releases['r'] as $release) { + if (!isset($latest)) { + if ($dostable && $release['s'] == 'stable') { + $latest = $release['v']; + $state = 'stable'; + } + if (!$dostable) { + $latest = $release['v']; + $state = $release['s']; + } + } + if (!isset($stable) && $release['s'] == 'stable') { + $stable = $release['v']; + if (!isset($unstable)) { + $unstable = $stable; + } + } + if (!isset($unstable) && $release['s'] != 'stable') { + $latest = $unstable = $release['v']; + $state = $release['s']; + } + if (isset($latest) && !isset($state)) { + $state = $release['s']; + } + if (isset($latest) && isset($stable) && isset($unstable)) { + break; + } + } + $deps = array(); + if (!isset($unstable)) { + $unstable = false; + $state = 'stable'; + if (isset($stable)) { + $latest = $unstable = $stable; + } + } else { + $latest = $unstable; + } + if (!isset($latest)) { + $latest = false; + } + if ($latest) { + $d = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) . '/deps.' . + $latest . '.txt'); + if (!PEAR::isError($d)) { + $d = unserialize($d); + if ($d) { + if (isset($d['required'])) { + if (!class_exists('PEAR_PackageFile_v2')) { + require_once 'PEAR/PackageFile/v2.php'; + } + if (!isset($pf)) { + $pf = new PEAR_PackageFile_v2; + } + $pf->setDeps($d); + $tdeps = $pf->getDeps(); + } else { + $tdeps = $d; + } + foreach ($tdeps as $dep) { + if ($dep['type'] !== 'pkg') { + continue; + } + $deps[] = $dep; + } + } + } + } + if (!isset($stable)) { + $stable = '-n/a-'; + } + if (!$searchpackage) { + $info = array('stable' => $latest, 'summary' => $inf['s'], 'description' => + $inf['d'], 'deps' => $deps, 'category' => $inf['ca']['_content'], + 'unstable' => $unstable, 'state' => $state); + } else { + $info = array('stable' => $stable, 'summary' => $inf['s'], 'description' => + $inf['d'], 'deps' => $deps, 'category' => $inf['ca']['_content'], + 'unstable' => $unstable, 'state' => $state); + } + } + $ret[$package] = $info; + } + PEAR::popErrorHandling(); + return $ret; + } + + function listLatestUpgrades($base, $pref_state, $installed, $channel, &$reg) + { + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml'); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + $ret = array(); + if (!is_array($packagelist) || !isset($packagelist['p'])) { + return $ret; + } + if (!is_array($packagelist['p'])) { + $packagelist['p'] = array($packagelist['p']); + } + foreach ($packagelist['p'] as $package) { + if (!isset($installed[strtolower($package)])) { + continue; + } + $inst_version = $reg->packageInfo($package, 'version', $channel); + $inst_state = $reg->packageInfo($package, 'release_state', $channel); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $info = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases.xml'); + PEAR::popErrorHandling(); + if (PEAR::isError($info)) { + continue; // no remote releases + } + if (!isset($info['r'])) { + continue; + } + $found = false; + $release = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + // $info['r'] is sorted by version number + foreach ($info['r'] as $release) { + if ($inst_version && version_compare($release['v'], $inst_version, '<=')) { + // not newer than the one installed + break; + } + + // new version > installed version + if (!$pref_state) { + // every state is a good state + $found = true; + break; + } else { + $new_state = $release['s']; + // if new state >= installed state: go + if (in_array($new_state, $this->betterStates($inst_state, true))) { + $found = true; + break; + } else { + // only allow to lower the state of package, + // if new state >= preferred state: go + if (in_array($new_state, $this->betterStates($pref_state, true))) { + $found = true; + break; + } + } + } + } + if (!$found) { + continue; + } + $relinfo = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) . '/' . + $release['v'] . '.xml'); + if (PEAR::isError($relinfo)) { + return $relinfo; + } + $ret[$package] = array( + 'version' => $release['v'], + 'state' => $release['s'], + 'filesize' => $relinfo['f'], + ); + } + return $ret; + } + + function packageInfo($base, $package) + { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pinfo = $this->_rest->retrieveData($base . 'p/' . strtolower($package) . '/info.xml'); + if (PEAR::isError($pinfo)) { + PEAR::popErrorHandling(); + return PEAR::raiseError('Unknown package: "' . $package . '" (Debug: ' . + $pinfo->getMessage() . ')'); + } + $releases = array(); + $allreleases = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases.xml'); + if (!PEAR::isError($allreleases)) { + if (!class_exists('PEAR_PackageFile_v2')) { + require_once 'PEAR/PackageFile/v2.php'; + } + if (!is_array($allreleases['r']) || !isset($allreleases['r'][0])) { + $allreleases['r'] = array($allreleases['r']); + } + $pf = new PEAR_PackageFile_v2; + foreach ($allreleases['r'] as $release) { + $ds = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) . '/deps.' . + $release['v'] . '.txt'); + if (PEAR::isError($ds)) { + continue; + } + if (!isset($latest)) { + $latest = $release['v']; + } + $pf->setDeps(unserialize($ds)); + $ds = $pf->getDeps(); + $info = $this->_rest->retrieveCacheFirst($base . 'r/' . strtolower($package) + . '/' . $release['v'] . '.xml'); + if (PEAR::isError($info)) { + continue; + } + $releases[$release['v']] = array( + 'doneby' => $info['m'], + 'license' => $info['l'], + 'summary' => $info['s'], + 'description' => $info['d'], + 'releasedate' => $info['da'], + 'releasenotes' => $info['n'], + 'state' => $release['s'], + 'deps' => $ds ? $ds : array(), + ); + } + } else { + $latest = ''; + } + PEAR::popErrorHandling(); + if (isset($pinfo['dc']) && isset($pinfo['dp'])) { + if (is_array($pinfo['dp'])) { + $deprecated = array('channel' => (string) $pinfo['dc'], + 'package' => trim($pinfo['dp']['_content'])); + } else { + $deprecated = array('channel' => (string) $pinfo['dc'], + 'package' => trim($pinfo['dp'])); + } + } else { + $deprecated = false; + } + return array( + 'name' => $pinfo['n'], + 'channel' => $pinfo['c'], + 'category' => $pinfo['ca']['_content'], + 'stable' => $latest, + 'license' => $pinfo['l'], + 'summary' => $pinfo['s'], + 'description' => $pinfo['d'], + 'releases' => $releases, + 'deprecated' => $deprecated, + ); + } + + /** + * Return an array containing all of the states that are more stable than + * or equal to the passed in state + * + * @param string Release state + * @param boolean Determines whether to include $state in the list + * @return false|array False if $state is not a valid release state + */ + function betterStates($state, $include = false) + { + static $states = array('snapshot', 'devel', 'alpha', 'beta', 'stable'); + $i = array_search($state, $states); + if ($i === false) { + return false; + } + if ($include) { + $i--; + } + return array_slice($states, $i + 1); + } +} +?> \ No newline at end of file diff --git a/PEAR/REST/11.php b/PEAR/REST/11.php new file mode 100644 index 0000000..39f4e22 --- /dev/null +++ b/PEAR/REST/11.php @@ -0,0 +1,317 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: 11.php,v 1.12 2007/06/19 04:31:49 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.3 + */ + +/** + * For downloading REST xml/txt files + */ +require_once 'PEAR/REST.php'; + +/** + * Implement REST 1.1 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.3 + */ +class PEAR_REST_11 +{ + /** + * @var PEAR_REST + */ + var $_rest; + + function PEAR_REST_11($config, $options = array()) + { + $this->_rest = &new PEAR_REST($config, $options); + } + + function listAll($base, $dostable, $basic = true) + { + $categorylist = $this->_rest->retrieveData($base . 'c/categories.xml'); + if (PEAR::isError($categorylist)) { + return $categorylist; + } + $ret = array(); + if (!is_array($categorylist['c']) || !isset($categorylist['c'][0])) { + $categorylist['c'] = array($categorylist['c']); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + + foreach ($categorylist['c'] as $progress => $category) { + $category = $category['_content']; + $packagesinfo = $this->_rest->retrieveData($base . + 'c/' . urlencode($category) . '/packagesinfo.xml'); + + if (PEAR::isError($packagesinfo)) { + continue; + } + + if (!is_array($packagesinfo) || !isset($packagesinfo['pi'])) { + continue; + } + + if (!is_array($packagesinfo['pi']) || !isset($packagesinfo['pi'][0])) { + $packagesinfo['pi'] = array($packagesinfo['pi']); + } + + foreach ($packagesinfo['pi'] as $packageinfo) { + $info = $packageinfo['p']; + $package = $info['n']; + $releases = isset($packageinfo['a']) ? $packageinfo['a'] : false; + unset($latest); + unset($unstable); + unset($stable); + unset($state); + + if ($releases) { + if (!isset($releases['r'][0])) { + $releases['r'] = array($releases['r']); + } + foreach ($releases['r'] as $release) { + if (!isset($latest)) { + if ($dostable && $release['s'] == 'stable') { + $latest = $release['v']; + $state = 'stable'; + } + if (!$dostable) { + $latest = $release['v']; + $state = $release['s']; + } + } + if (!isset($stable) && $release['s'] == 'stable') { + $stable = $release['v']; + if (!isset($unstable)) { + $unstable = $stable; + } + } + if (!isset($unstable) && $release['s'] != 'stable') { + $unstable = $release['v']; + $state = $release['s']; + } + if (isset($latest) && !isset($state)) { + $state = $release['s']; + } + if (isset($latest) && isset($stable) && isset($unstable)) { + break; + } + } + } + + if ($basic) { // remote-list command + if (!isset($latest)) { + $latest = false; + } + if ($dostable) { + // $state is not set if there are no releases + if (isset($state) && $state == 'stable') { + $ret[$package] = array('stable' => $latest); + } else { + $ret[$package] = array('stable' => '-n/a-'); + } + } else { + $ret[$package] = array('stable' => $latest); + } + continue; + } + + // list-all command + $deps = array(); + if (!isset($unstable)) { + $unstable = false; + $state = 'stable'; + if (isset($stable)) { + $latest = $unstable = $stable; + } + } else { + $latest = $unstable; + } + + if (!isset($latest)) { + $latest = false; + } + + if ($latest && isset($packageinfo['deps'])) { + if (!is_array($packageinfo['deps']) || + !isset($packageinfo['deps'][0])) { + $packageinfo['deps'] = array($packageinfo['deps']); + } + $d = false; + foreach ($packageinfo['deps'] as $dep) { + if ($dep['v'] == $latest) { + $d = unserialize($dep['d']); + } + } + if ($d) { + if (isset($d['required'])) { + if (!class_exists('PEAR_PackageFile_v2')) { + require_once 'PEAR/PackageFile/v2.php'; + } + if (!isset($pf)) { + $pf = new PEAR_PackageFile_v2; + } + $pf->setDeps($d); + $tdeps = $pf->getDeps(); + } else { + $tdeps = $d; + } + foreach ($tdeps as $dep) { + if ($dep['type'] !== 'pkg') { + continue; + } + $deps[] = $dep; + } + } + } + + $info = array('stable' => $latest, 'summary' => $info['s'], + 'description' => + $info['d'], 'deps' => $deps, 'category' => $info['ca']['_content'], + 'unstable' => $unstable, 'state' => $state); + $ret[$package] = $info; + } + } + PEAR::popErrorHandling(); + return $ret; + } + + /** + * List all categories of a REST server + * + * @param string $base base URL of the server + * @return array of categorynames + */ + function listCategories($base) + { + $categorylist = $this->_rest->retrieveData($base . 'c/categories.xml'); + if (PEAR::isError($categorylist)) { + return $categorylist; + } + if (!is_array($categorylist) || !isset($categorylist['c'])) { + return array(); + } + if (isset($categorylist['c']['_content'])) { + // only 1 category + $categorylist['c'] = array($categorylist['c']); + } + return $categorylist['c']; + } + + /** + * List packages in a category of a REST server + * + * @param string $base base URL of the server + * @param string $category name of the category + * @param boolean $info also download full package info + * @return array of packagenames + */ + function listCategory($base, $category, $info=false) + { + if ($info == false) { + $url = '%s'.'c/%s/packages.xml'; + } else { + $url = '%s'.'c/%s/packagesinfo.xml'; + } + $url = sprintf($url, + $base, + urlencode($category)); + + // gives '404 Not Found' error when category doesn't exist + $packagelist = $this->_rest->retrieveData($url); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if (!is_array($packagelist)) { + return array(); + } + + if ($info == false) { + if (!isset($packagelist['p'])) { + return array(); + } + if (!is_array($packagelist['p']) || + !isset($packagelist['p'][0])) { // only 1 pkg + $packagelist = array($packagelist['p']); + } else { + $packagelist = $packagelist['p']; + } + return $packagelist; + } else { + // info == true + if (!isset($packagelist['pi'])) { + return array(); + } + if (!is_array($packagelist['pi']) || + !isset($packagelist['pi'][0])) { // only 1 pkg + $packagelist_pre = array($packagelist['pi']); + } else { + $packagelist_pre = $packagelist['pi']; + } + + $packagelist = array(); + foreach ($packagelist_pre as $i => $item) { + // compatibility with r/.xml + if (isset($item['a']['r'][0])) { + // multiple releases + $item['p']['v'] = $item['a']['r'][0]['v']; + $item['p']['st'] = $item['a']['r'][0]['s']; + } elseif (isset($item['a'])) { + // first and only release + $item['p']['v'] = $item['a']['r']['v']; + $item['p']['st'] = $item['a']['r']['s']; + } + + $packagelist[$i] = array('attribs' => $item['p']['r'], + '_content' => $item['p']['n'], + 'info' => $item['p']); + } + } + + return $packagelist; + } + + /** + * Return an array containing all of the states that are more stable than + * or equal to the passed in state + * + * @param string Release state + * @param boolean Determines whether to include $state in the list + * @return false|array False if $state is not a valid release state + */ + function betterStates($state, $include = false) + { + static $states = array('snapshot', 'devel', 'alpha', 'beta', 'stable'); + $i = array_search($state, $states); + if ($i === false) { + return false; + } + if ($include) { + $i--; + } + return array_slice($states, $i + 1); + } +} +?> \ No newline at end of file diff --git a/PEAR/REST/13.php b/PEAR/REST/13.php new file mode 100644 index 0000000..6e18e1b --- /dev/null +++ b/PEAR/REST/13.php @@ -0,0 +1,291 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: 13.php,v 1.2 2007/05/31 03:51:08 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a12 + */ + +/** + * For downloading REST xml/txt files + */ +require_once 'PEAR/REST.php'; +require_once 'PEAR/REST/10.php'; + +/** + * Implement REST 1.3 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a12 + */ +class PEAR_REST_13 extends PEAR_REST_10 +{ + /** + * Retrieve information about a remote package to be downloaded from a REST server + * + * This is smart enough to resolve the minimum PHP version dependency prior to download + * @param string $base The uri to prepend to all REST calls + * @param array $packageinfo an array of format: + *
+     *  array(
+     *   'package' => 'packagename',
+     *   'channel' => 'channelname',
+     *  ['state' => 'alpha' (or valid state),]
+     *  -or-
+     *  ['version' => '1.whatever']
+     * 
+ * @param string $prefstate Current preferred_state config variable value + * @param bool $installed the installed version of this package to compare against + * @return array|false|PEAR_Error see {@link _returnDownloadURL()} + */ + function getDownloadURL($base, $packageinfo, $prefstate, $installed) + { + $channel = $packageinfo['channel']; + $package = $packageinfo['package']; + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + $state = $version = null; + if (isset($packageinfo['state'])) { + $state = $packageinfo['state']; + } + if (isset($packageinfo['version'])) { + $version = $packageinfo['version']; + } + $info = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases2.xml'); + if (PEAR::isError($info)) { + return PEAR::raiseError('No releases available for package "' . + $channel . '/' . $package . '"'); + } + if (!isset($info['r'])) { + return false; + } + $found = false; + $release = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + $skippedphp = false; + foreach ($info['r'] as $release) { + if (!isset($this->_rest->_options['force']) && ($installed && + version_compare($release['v'], $installed, '<'))) { + continue; + } + if (isset($state)) { + // try our preferred state first + if ($release['s'] == $state) { + if (!isset($version) && version_compare($release['m'], phpversion(), '>')) { + // skip releases that require a PHP version newer than our PHP version + $skippedphp = $release; + continue; + } + $found = true; + break; + } + // see if there is something newer and more stable + // bug #7221 + if (in_array($release['s'], $this->betterStates($state), true)) { + if (!isset($version) && version_compare($release['m'], phpversion(), '>')) { + // skip releases that require a PHP version newer than our PHP version + $skippedphp = $release; + continue; + } + $found = true; + break; + } + } elseif (isset($version)) { + if ($release['v'] == $version) { + if (!isset($this->_rest->_options['force']) && + !isset($version) && + version_compare($release['m'], phpversion(), '>')) { + // skip releases that require a PHP version newer than our PHP version + $skippedphp = $release; + continue; + } + $found = true; + break; + } + } else { + if (in_array($release['s'], $states)) { + if (version_compare($release['m'], phpversion(), '>')) { + // skip releases that require a PHP version newer than our PHP version + $skippedphp = $release; + continue; + } + $found = true; + break; + } + } + } + if (!$found && $skippedphp) { + $found = null; + } + return $this->_returnDownloadURL($base, $package, $release, $info, $found, $skippedphp); + } + + function getDepDownloadURL($base, $xsdversion, $dependency, $deppackage, + $prefstate = 'stable', $installed = false) + { + $channel = $dependency['channel']; + $package = $dependency['name']; + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + $state = $version = null; + if (isset($packageinfo['state'])) { + $state = $packageinfo['state']; + } + if (isset($packageinfo['version'])) { + $version = $packageinfo['version']; + } + $info = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases2.xml'); + if (PEAR::isError($info)) { + return PEAR::raiseError('Package "' . $deppackage['channel'] . '/' . $deppackage['package'] + . '" dependency "' . $channel . '/' . $package . '" has no releases'); + } + if (!is_array($info) || !isset($info['r'])) { + return false; + } + $exclude = array(); + $min = $max = $recommended = false; + if ($xsdversion == '1.0') { + $pinfo['package'] = $dependency['name']; + $pinfo['channel'] = 'pear.php.net'; // this is always true - don't change this + switch ($dependency['rel']) { + case 'ge' : + $min = $dependency['version']; + break; + case 'gt' : + $min = $dependency['version']; + $exclude = array($dependency['version']); + break; + case 'eq' : + $recommended = $dependency['version']; + break; + case 'lt' : + $max = $dependency['version']; + $exclude = array($dependency['version']); + break; + case 'le' : + $max = $dependency['version']; + break; + case 'ne' : + $exclude = array($dependency['version']); + break; + } + } else { + $pinfo['package'] = $dependency['name']; + $min = isset($dependency['min']) ? $dependency['min'] : false; + $max = isset($dependency['max']) ? $dependency['max'] : false; + $recommended = isset($dependency['recommended']) ? + $dependency['recommended'] : false; + if (isset($dependency['exclude'])) { + if (!isset($dependency['exclude'][0])) { + $exclude = array($dependency['exclude']); + } + } + } + $found = false; + $release = false; + $skippedphp = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + foreach ($info['r'] as $release) { + if (!isset($this->_rest->_options['force']) && ($installed && + version_compare($release['v'], $installed, '<'))) { + continue; + } + if (in_array($release['v'], $exclude)) { // skip excluded versions + continue; + } + // allow newer releases to say "I'm OK with the dependent package" + if ($xsdversion == '2.0' && isset($release['co'])) { + if (!is_array($release['co']) || !isset($release['co'][0])) { + $release['co'] = array($release['co']); + } + foreach ($release['co'] as $entry) { + if (isset($entry['x']) && !is_array($entry['x'])) { + $entry['x'] = array($entry['x']); + } elseif (!isset($entry['x'])) { + $entry['x'] = array(); + } + if ($entry['c'] == $deppackage['channel'] && + strtolower($entry['p']) == strtolower($deppackage['package']) && + version_compare($deppackage['version'], $entry['min'], '>=') && + version_compare($deppackage['version'], $entry['max'], '<=') && + !in_array($release['v'], $entry['x'])) { + if (version_compare($release['m'], phpversion(), '>')) { + // skip dependency releases that require a PHP version + // newer than our PHP version + $skippedphp = $release; + continue; + } + $recommended = $release['v']; + break; + } + } + } + if ($recommended) { + if ($release['v'] != $recommended) { // if we want a specific + // version, then skip all others + continue; + } else { + if (!in_array($release['s'], $states)) { + // the stability is too low, but we must return the + // recommended version if possible + return $this->_returnDownloadURL($base, $package, $release, $info, true); + } + } + } + if ($min && version_compare($release['v'], $min, 'lt')) { // skip too old versions + continue; + } + if ($max && version_compare($release['v'], $max, 'gt')) { // skip too new versions + continue; + } + if ($installed && version_compare($release['v'], $installed, '<')) { + continue; + } + if (in_array($release['s'], $states)) { // if in the preferred state... + if (version_compare($release['m'], phpversion(), '>')) { + // skip dependency releases that require a PHP version + // newer than our PHP version + $skippedphp = $release; + continue; + } + $found = true; // ... then use it + break; + } + } + if (!$found && $skippedphp) { + $found = null; + } + return $this->_returnDownloadURL($base, $package, $release, $info, $found, $skippedphp); + } +} +?> \ No newline at end of file diff --git a/PEAR/Registry.php b/PEAR/Registry.php new file mode 100644 index 0000000..7de7073 --- /dev/null +++ b/PEAR/Registry.php @@ -0,0 +1,2224 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Registry.php,v 1.166 2007/06/16 18:41:59 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * for PEAR_Error + */ +require_once 'PEAR.php'; +require_once 'PEAR/DependencyDB.php'; + +define('PEAR_REGISTRY_ERROR_LOCK', -2); +define('PEAR_REGISTRY_ERROR_FORMAT', -3); +define('PEAR_REGISTRY_ERROR_FILE', -4); +define('PEAR_REGISTRY_ERROR_CONFLICT', -5); +define('PEAR_REGISTRY_ERROR_CHANNEL_FILE', -6); + +/** + * Administration class used to maintain the installed package database. + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Registry extends PEAR +{ + // {{{ properties + + /** + * File containing all channel information. + * @var string + */ + var $channels = ''; + + /** Directory where registry files are stored. + * @var string + */ + var $statedir = ''; + + /** File where the file map is stored + * @var string + */ + var $filemap = ''; + + /** Directory where registry files for channels are stored. + * @var string + */ + var $channelsdir = ''; + + /** Name of file used for locking the registry + * @var string + */ + var $lockfile = ''; + + /** File descriptor used during locking + * @var resource + */ + var $lock_fp = null; + + /** Mode used during locking + * @var int + */ + var $lock_mode = 0; // XXX UNUSED + + /** Cache of package information. Structure: + * array( + * 'package' => array('id' => ... ), + * ... ) + * @var array + */ + var $pkginfo_cache = array(); + + /** Cache of file map. Structure: + * array( '/path/to/file' => 'package', ... ) + * @var array + */ + var $filemap_cache = array(); + + /** + * @var false|PEAR_ChannelFile + */ + var $_pearChannel; + + /** + * @var false|PEAR_ChannelFile + */ + var $_peclChannel; + + /** + * @var PEAR_DependencyDB + */ + var $_dependencyDB; + + /** + * @var PEAR_Config + */ + var $_config; + // }}} + + // {{{ constructor + + /** + * PEAR_Registry constructor. + * + * @param string (optional) PEAR install directory (for .php files) + * @param PEAR_ChannelFile PEAR_ChannelFile object representing the PEAR channel, if + * default values are not desired. Only used the very first time a PEAR + * repository is initialized + * @param PEAR_ChannelFile PEAR_ChannelFile object representing the PECL channel, if + * default values are not desired. Only used the very first time a PEAR + * repository is initialized + * + * @access public + */ + function PEAR_Registry($pear_install_dir = PEAR_INSTALL_DIR, $pear_channel = false, + $pecl_channel = false) + { + parent::PEAR(); + $ds = DIRECTORY_SEPARATOR; + $this->install_dir = $pear_install_dir; + $this->channelsdir = $pear_install_dir.$ds.'.channels'; + $this->statedir = $pear_install_dir.$ds.'.registry'; + $this->filemap = $pear_install_dir.$ds.'.filemap'; + $this->lockfile = $pear_install_dir.$ds.'.lock'; + $this->_pearChannel = $pear_channel; + $this->_peclChannel = $pecl_channel; + $this->_config = false; + } + + function hasWriteAccess() + { + if (!file_exists($this->install_dir)) { + $dir = $this->install_dir; + while ($dir && $dir != '.') { + $olddir = $dir; + $dir = dirname($dir); // cd .. + if ($dir != '.' && file_exists($dir)) { + if (is_writeable($dir)) { + return true; + } else { + return false; + } + } + if ($dir == $olddir) { // this can happen in safe mode + return @is_writable($dir); + } + } + return false; + } + return is_writeable($this->install_dir); + } + + function setConfig(&$config) + { + $this->_config = &$config; + } + + function _initializeChannelDirs() + { + static $running = false; + if (!$running) { + $running = true; + $ds = DIRECTORY_SEPARATOR; + if (!is_dir($this->channelsdir) || + !file_exists($this->channelsdir . $ds . 'pear.php.net.reg') || + !file_exists($this->channelsdir . $ds . 'pecl.php.net.reg') || + !file_exists($this->channelsdir . $ds . '__uri.reg')) { + if (!file_exists($this->channelsdir . $ds . 'pear.php.net.reg')) { + $pear_channel = $this->_pearChannel; + if (!is_a($pear_channel, 'PEAR_ChannelFile') || !$pear_channel->validate()) { + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $pear_channel = new PEAR_ChannelFile; + $pear_channel->setName('pear.php.net'); + $pear_channel->setAlias('pear'); + $pear_channel->setServer('pear.php.net'); + $pear_channel->setSummary('PHP Extension and Application Repository'); + $pear_channel->setDefaultPEARProtocols(); + $pear_channel->setBaseURL('REST1.0', 'http://pear.php.net/rest/'); + $pear_channel->setBaseURL('REST1.1', 'http://pear.php.net/rest/'); + } else { + $pear_channel->setName('pear.php.net'); + $pear_channel->setAlias('pear'); + } + $pear_channel->validate(); + $this->_addChannel($pear_channel); + } + if (!file_exists($this->channelsdir . $ds . 'pecl.php.net.reg')) { + $pecl_channel = $this->_peclChannel; + if (!is_a($pecl_channel, 'PEAR_ChannelFile') || !$pecl_channel->validate()) { + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $pecl_channel = new PEAR_ChannelFile; + $pecl_channel->setName('pecl.php.net'); + $pecl_channel->setAlias('pecl'); + $pecl_channel->setServer('pecl.php.net'); + $pecl_channel->setSummary('PHP Extension Community Library'); + $pecl_channel->setDefaultPEARProtocols(); + $pecl_channel->setBaseURL('REST1.0', 'http://pecl.php.net/rest/'); + $pecl_channel->setBaseURL('REST1.1', 'http://pecl.php.net/rest/'); + $pecl_channel->setValidationPackage('PEAR_Validator_PECL', '1.0'); + } else { + $pecl_channel->setName('pecl.php.net'); + $pecl_channel->setAlias('pecl'); + } + $pecl_channel->validate(); + $this->_addChannel($pecl_channel); + } + if (!file_exists($this->channelsdir . $ds . '__uri.reg')) { + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $private = new PEAR_ChannelFile; + $private->setName('__uri'); + $private->addFunction('xmlrpc', '1.0', '****'); + $private->setSummary('Pseudo-channel for static packages'); + $this->_addChannel($private); + } + $this->_rebuildFileMap(); + } + $running = false; + } + } + + function _initializeDirs() + { + $ds = DIRECTORY_SEPARATOR; + // XXX Compatibility code should be removed in the future + // rename all registry files if any to lowercase + if (!OS_WINDOWS && file_exists($this->statedir) && is_dir($this->statedir) && + $handle = opendir($this->statedir)) { + $dest = $this->statedir . $ds; + while (false !== ($file = readdir($handle))) { + if (preg_match('/^.*[A-Z].*\.reg\\z/', $file)) { + rename($dest . $file, $dest . strtolower($file)); + } + } + closedir($handle); + } + $this->_initializeChannelDirs(); + if (!file_exists($this->filemap)) { + $this->_rebuildFileMap(); + } + $this->_initializeDepDB(); + } + + function _initializeDepDB() + { + if (!isset($this->_dependencyDB)) { + static $initializing = false; + if (!$initializing) { + $initializing = true; + if (!$this->_config) { // never used? + if (OS_WINDOWS) { + $file = 'pear.ini'; + } else { + $file = '.pearrc'; + } + $this->_config = &new PEAR_Config($this->statedir . DIRECTORY_SEPARATOR . + $file); + $this->_config->setRegistry($this); + $this->_config->set('php_dir', $this->install_dir); + } + $this->_dependencyDB = &PEAR_DependencyDB::singleton($this->_config); + if (PEAR::isError($this->_dependencyDB)) { + // attempt to recover by removing the dep db + if (file_exists($this->_config->get('php_dir', null, 'pear.php.net') . + DIRECTORY_SEPARATOR . '.depdb')) { + @unlink($this->_config->get('php_dir', null, 'pear.php.net') . + DIRECTORY_SEPARATOR . '.depdb'); + } + $this->_dependencyDB = &PEAR_DependencyDB::singleton($this->_config); + if (PEAR::isError($this->_dependencyDB)) { + echo $this->_dependencyDB->getMessage(); + echo 'Unrecoverable error'; + exit(1); + } + } + $initializing = false; + } + } + } + // }}} + // {{{ destructor + + /** + * PEAR_Registry destructor. Makes sure no locks are forgotten. + * + * @access private + */ + function _PEAR_Registry() + { + parent::_PEAR(); + if (is_resource($this->lock_fp)) { + $this->_unlock(); + } + } + + // }}} + + // {{{ _assertStateDir() + + /** + * Make sure the directory where we keep registry files exists. + * + * @return bool TRUE if directory exists, FALSE if it could not be + * created + * + * @access private + */ + function _assertStateDir($channel = false) + { + if ($channel && $this->_getChannelFromAlias($channel) != 'pear.php.net') { + return $this->_assertChannelStateDir($channel); + } + static $init = false; + if (!file_exists($this->statedir)) { + if (!$this->hasWriteAccess()) { + return false; + } + require_once 'System.php'; + if (!System::mkdir(array('-p', $this->statedir))) { + return $this->raiseError("could not create directory '{$this->statedir}'"); + } + $init = true; + } elseif (!is_dir($this->statedir)) { + return $this->raiseError('Cannot create directory ' . $this->statedir . ', ' . + 'it already exists and is not a directory'); + } + $ds = DIRECTORY_SEPARATOR; + if (!file_exists($this->channelsdir)) { + if (!file_exists($this->channelsdir . $ds . 'pear.php.net.reg') || + !file_exists($this->channelsdir . $ds . 'pecl.php.net.reg') || + !file_exists($this->channelsdir . $ds . '__uri.reg')) { + $init = true; + } + } elseif (!is_dir($this->channelsdir)) { + return $this->raiseError('Cannot create directory ' . $this->channelsdir . ', ' . + 'it already exists and is not a directory'); + } + if ($init) { + static $running = false; + if (!$running) { + $running = true; + $this->_initializeDirs(); + $running = false; + $init = false; + } + } else { + $this->_initializeDepDB(); + } + return true; + } + + // }}} + // {{{ _assertChannelStateDir() + + /** + * Make sure the directory where we keep registry files exists for a non-standard channel. + * + * @param string channel name + * @return bool TRUE if directory exists, FALSE if it could not be + * created + * + * @access private + */ + function _assertChannelStateDir($channel) + { + $ds = DIRECTORY_SEPARATOR; + if (!$channel || $this->_getChannelFromAlias($channel) == 'pear.php.net') { + if (!file_exists($this->channelsdir . $ds . 'pear.php.net.reg')) { + $this->_initializeChannelDirs(); + } + return $this->_assertStateDir($channel); + } + $channelDir = $this->_channelDirectoryName($channel); + if (!is_dir($this->channelsdir) || + !file_exists($this->channelsdir . $ds . 'pear.php.net.reg')) { + $this->_initializeChannelDirs(); + } + if (!file_exists($channelDir)) { + if (!$this->hasWriteAccess()) { + return false; + } + require_once 'System.php'; + if (!System::mkdir(array('-p', $channelDir))) { + return $this->raiseError("could not create directory '" . $channelDir . + "'"); + } + } elseif (!is_dir($channelDir)) { + return $this->raiseError("could not create directory '" . $channelDir . + "', already exists and is not a directory"); + } + return true; + } + + // }}} + // {{{ _assertChannelDir() + + /** + * Make sure the directory where we keep registry files for channels exists + * + * @return bool TRUE if directory exists, FALSE if it could not be + * created + * + * @access private + */ + function _assertChannelDir() + { + if (!file_exists($this->channelsdir)) { + if (!$this->hasWriteAccess()) { + return false; + } + require_once 'System.php'; + if (!System::mkdir(array('-p', $this->channelsdir))) { + return $this->raiseError("could not create directory '{$this->channelsdir}'"); + } + } elseif (!is_dir($this->channelsdir)) { + return $this->raiseError("could not create directory '{$this->channelsdir}" . + "', it already exists and is not a directory"); + + } + if (!file_exists($this->channelsdir . DIRECTORY_SEPARATOR . '.alias')) { + if (!$this->hasWriteAccess()) { + return false; + } + require_once 'System.php'; + if (!System::mkdir(array('-p', $this->channelsdir . DIRECTORY_SEPARATOR . '.alias'))) { + return $this->raiseError("could not create directory '{$this->channelsdir}/.alias'"); + } + } elseif (!is_dir($this->channelsdir . DIRECTORY_SEPARATOR . '.alias')) { + return $this->raiseError("could not create directory '{$this->channelsdir}" . + "/.alias', it already exists and is not a directory"); + + } + return true; + } + + // }}} + // {{{ _packageFileName() + + /** + * Get the name of the file where data for a given package is stored. + * + * @param string channel name, or false if this is a PEAR package + * @param string package name + * + * @return string registry file name + * + * @access public + */ + function _packageFileName($package, $channel = false) + { + if ($channel && $this->_getChannelFromAlias($channel) != 'pear.php.net') { + return $this->_channelDirectoryName($channel) . DIRECTORY_SEPARATOR . + strtolower($package) . '.reg'; + } + return $this->statedir . DIRECTORY_SEPARATOR . strtolower($package) . '.reg'; + } + + // }}} + // {{{ _channelFileName() + + /** + * Get the name of the file where data for a given channel is stored. + * @param string channel name + * @return string registry file name + */ + function _channelFileName($channel, $noaliases = false) + { + if (!$noaliases) { + if (file_exists($this->_getChannelAliasFileName($channel))) { + $channel = implode('', file($this->_getChannelAliasFileName($channel))); + } + } + return $this->channelsdir . DIRECTORY_SEPARATOR . str_replace('/', '_', + strtolower($channel)) . '.reg'; + } + + // }}} + // {{{ getChannelAliasFileName() + + /** + * @param string + * @return string + */ + function _getChannelAliasFileName($alias) + { + return $this->channelsdir . DIRECTORY_SEPARATOR . '.alias' . + DIRECTORY_SEPARATOR . str_replace('/', '_', strtolower($alias)) . '.txt'; + } + + // }}} + // {{{ _getChannelFromAlias() + + /** + * Get the name of a channel from its alias + */ + function _getChannelFromAlias($channel) + { + if (!$this->_channelExists($channel)) { + if ($channel == 'pear.php.net') { + return 'pear.php.net'; + } + if ($channel == 'pecl.php.net') { + return 'pecl.php.net'; + } + if ($channel == '__uri') { + return '__uri'; + } + return false; + } + $channel = strtolower($channel); + if (file_exists($this->_getChannelAliasFileName($channel))) { + // translate an alias to an actual channel + return implode('', file($this->_getChannelAliasFileName($channel))); + } else { + return $channel; + } + } + // }}} + // {{{ _getChannelFromAlias() + + /** + * Get the alias of a channel from its alias or its name + */ + function _getAlias($channel) + { + if (!$this->_channelExists($channel)) { + if ($channel == 'pear.php.net') { + return 'pear'; + } + if ($channel == 'pecl.php.net') { + return 'pecl'; + } + return false; + } + $channel = $this->_getChannel($channel); + if (PEAR::isError($channel)) { + return $channel; + } + return $channel->getAlias(); + } + // }}} + // {{{ _channelDirectoryName() + + /** + * Get the name of the file where data for a given package is stored. + * + * @param string channel name, or false if this is a PEAR package + * @param string package name + * + * @return string registry file name + * + * @access public + */ + function _channelDirectoryName($channel) + { + if (!$channel || $this->_getChannelFromAlias($channel) == 'pear.php.net') { + return $this->statedir; + } else { + $ch = $this->_getChannelFromAlias($channel); + if (!$ch) { + $ch = $channel; + } + return $this->statedir . DIRECTORY_SEPARATOR . strtolower('.channel.' . + str_replace('/', '_', $ch)); + } + } + + // }}} + // {{{ _openPackageFile() + + function _openPackageFile($package, $mode, $channel = false) + { + if (!$this->_assertStateDir($channel)) { + return null; + } + if (!in_array($mode, array('r', 'rb')) && !$this->hasWriteAccess()) { + return null; + } + $file = $this->_packageFileName($package, $channel); + if (!file_exists($file) && $mode == 'r' || $mode == 'rb') { + return null; + } + $fp = @fopen($file, $mode); + if (!$fp) { + return null; + } + return $fp; + } + + // }}} + // {{{ _closePackageFile() + + function _closePackageFile($fp) + { + fclose($fp); + } + + // }}} + // {{{ _openChannelFile() + + function _openChannelFile($channel, $mode) + { + if (!$this->_assertChannelDir()) { + return null; + } + if (!in_array($mode, array('r', 'rb')) && !$this->hasWriteAccess()) { + return null; + } + $file = $this->_channelFileName($channel); + if (!file_exists($file) && $mode == 'r' || $mode == 'rb') { + return null; + } + $fp = @fopen($file, $mode); + if (!$fp) { + return null; + } + return $fp; + } + + // }}} + // {{{ _closePackageFile() + + function _closeChannelFile($fp) + { + fclose($fp); + } + + // }}} + // {{{ _rebuildFileMap() + + function _rebuildFileMap() + { + if (!class_exists('PEAR_Installer_Role')) { + require_once 'PEAR/Installer/Role.php'; + } + $channels = $this->_listAllPackages(); + $files = array(); + foreach ($channels as $channel => $packages) { + foreach ($packages as $package) { + $version = $this->_packageInfo($package, 'version', $channel); + $filelist = $this->_packageInfo($package, 'filelist', $channel); + if (!is_array($filelist)) { + continue; + } + foreach ($filelist as $name => $attrs) { + if (isset($attrs['attribs'])) { + $attrs = $attrs['attribs']; + } + // it is possible for conflicting packages in different channels to + // conflict with data files/doc files + if ($name == 'dirtree') { + continue; + } + if (isset($attrs['role']) && !in_array($attrs['role'], + PEAR_Installer_Role::getInstallableRoles())) { + // these are not installed + continue; + } + if (isset($attrs['role']) && !in_array($attrs['role'], + PEAR_Installer_Role::getBaseinstallRoles())) { + $attrs['baseinstalldir'] = $package; + } + if (isset($attrs['baseinstalldir'])) { + $file = $attrs['baseinstalldir'].DIRECTORY_SEPARATOR.$name; + } else { + $file = $name; + } + $file = preg_replace(',^/+,', '', $file); + if ($channel != 'pear.php.net') { + if (!isset($files[$attrs['role']])) { + $files[$attrs['role']] = array(); + } + $files[$attrs['role']][$file] = array(strtolower($channel), + strtolower($package)); + } else { + if (!isset($files[$attrs['role']])) { + $files[$attrs['role']] = array(); + } + $files[$attrs['role']][$file] = strtolower($package); + } + } + } + } + $this->_assertStateDir(); + if (!$this->hasWriteAccess()) { + return false; + } + $fp = @fopen($this->filemap, 'wb'); + if (!$fp) { + return false; + } + $this->filemap_cache = $files; + fwrite($fp, serialize($files)); + fclose($fp); + return true; + } + + // }}} + // {{{ _readFileMap() + + function _readFileMap() + { + if (!file_exists($this->filemap)) { + return array(); + } + $fp = @fopen($this->filemap, 'r'); + if (!$fp) { + return $this->raiseError('PEAR_Registry: could not open filemap "' . $this->filemap . '"', PEAR_REGISTRY_ERROR_FILE, null, null, $php_errormsg); + } + clearstatcache(); + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + $fsize = filesize($this->filemap); + fclose($fp); + $data = file_get_contents($this->filemap); + set_magic_quotes_runtime($rt); + $tmp = unserialize($data); + if (!$tmp && $fsize > 7) { + return $this->raiseError('PEAR_Registry: invalid filemap data', PEAR_REGISTRY_ERROR_FORMAT, null, null, $data); + } + $this->filemap_cache = $tmp; + return true; + } + + // }}} + // {{{ _lock() + + /** + * Lock the registry. + * + * @param integer lock mode, one of LOCK_EX, LOCK_SH or LOCK_UN. + * See flock manual for more information. + * + * @return bool TRUE on success, FALSE if locking failed, or a + * PEAR error if some other error occurs (such as the + * lock file not being writable). + * + * @access private + */ + function _lock($mode = LOCK_EX) + { + if (!eregi('Windows 9', php_uname())) { + if ($mode != LOCK_UN && is_resource($this->lock_fp)) { + // XXX does not check type of lock (LOCK_SH/LOCK_EX) + return true; + } + if (!$this->_assertStateDir()) { + if ($mode == LOCK_EX) { + return $this->raiseError('Registry directory is not writeable by the current user'); + } else { + return true; + } + } + $open_mode = 'w'; + // XXX People reported problems with LOCK_SH and 'w' + if ($mode === LOCK_SH || $mode === LOCK_UN) { + if (!file_exists($this->lockfile)) { + touch($this->lockfile); + } + $open_mode = 'r'; + } + + if (!is_resource($this->lock_fp)) { + $this->lock_fp = @fopen($this->lockfile, $open_mode); + } + + if (!is_resource($this->lock_fp)) { + return $this->raiseError("could not create lock file" . + (isset($php_errormsg) ? ": " . $php_errormsg : "")); + } + if (!(int)flock($this->lock_fp, $mode)) { + switch ($mode) { + case LOCK_SH: $str = 'shared'; break; + case LOCK_EX: $str = 'exclusive'; break; + case LOCK_UN: $str = 'unlock'; break; + default: $str = 'unknown'; break; + } + return $this->raiseError("could not acquire $str lock ($this->lockfile)", + PEAR_REGISTRY_ERROR_LOCK); + } + } + return true; + } + + // }}} + // {{{ _unlock() + + function _unlock() + { + $ret = $this->_lock(LOCK_UN); + if (is_resource($this->lock_fp)) { + fclose($this->lock_fp); + } + $this->lock_fp = null; + return $ret; + } + + // }}} + // {{{ _packageExists() + + function _packageExists($package, $channel = false) + { + return file_exists($this->_packageFileName($package, $channel)); + } + + // }}} + // {{{ _channelExists() + + /** + * Determine whether a channel exists in the registry + * @param string Channel name + * @param bool if true, then aliases will be ignored + * @return boolean + */ + function _channelExists($channel, $noaliases = false) + { + $a = file_exists($this->_channelFileName($channel, $noaliases)); + if (!$a && $channel == 'pear.php.net') { + return true; + } + if (!$a && $channel == 'pecl.php.net') { + return true; + } + return $a; + } + + // }}} + // {{{ _addChannel() + + /** + * @param PEAR_ChannelFile Channel object + * @param donotuse + * @param string Last-Modified HTTP tag from remote request + * @return boolean|PEAR_Error True on creation, false if it already exists + */ + function _addChannel($channel, $update = false, $lastmodified = false) + { + if (!is_a($channel, 'PEAR_ChannelFile')) { + return false; + } + if (!$channel->validate()) { + return false; + } + if (file_exists($this->_channelFileName($channel->getName()))) { + if (!$update) { + return false; + } + $checker = $this->_getChannel($channel->getName()); + if (PEAR::isError($checker)) { + return $checker; + } + if ($channel->getAlias() != $checker->getAlias()) { + if (file_exists($this->_getChannelAliasFileName($checker->getAlias()))) { + @unlink($this->_getChannelAliasFileName($checker->getAlias())); + } + } + } else { + if ($update && !in_array($channel->getName(), array('pear.php.net', 'pecl.php.net'))) { + return false; + } + } + $ret = $this->_assertChannelDir(); + if (PEAR::isError($ret)) { + return $ret; + } + $ret = $this->_assertChannelStateDir($channel->getName()); + if (PEAR::isError($ret)) { + return $ret; + } + if ($channel->getAlias() != $channel->getName()) { + if (file_exists($this->_getChannelAliasFileName($channel->getAlias())) && + $this->_getChannelFromAlias($channel->getAlias()) != $channel->getName()) { + $channel->setAlias($channel->getName()); + } + if (!$this->hasWriteAccess()) { + return false; + } + $fp = @fopen($this->_getChannelAliasFileName($channel->getAlias()), 'w'); + if (!$fp) { + return false; + } + fwrite($fp, $channel->getName()); + fclose($fp); + } + if (!$this->hasWriteAccess()) { + return false; + } + $fp = @fopen($this->_channelFileName($channel->getName()), 'wb'); + if (!$fp) { + return false; + } + $info = $channel->toArray(); + if ($lastmodified) { + $info['_lastmodified'] = $lastmodified; + } else { + $info['_lastmodified'] = date('r'); + } + fwrite($fp, serialize($info)); + fclose($fp); + return true; + } + + // }}} + // {{{ _deleteChannel() + + /** + * Deletion fails if there are any packages installed from the channel + * @param string|PEAR_ChannelFile channel name + * @return boolean|PEAR_Error True on deletion, false if it doesn't exist + */ + function _deleteChannel($channel) + { + if (!is_string($channel)) { + if (is_a($channel, 'PEAR_ChannelFile')) { + if (!$channel->validate()) { + return false; + } + $channel = $channel->getName(); + } else { + return false; + } + } + if ($this->_getChannelFromAlias($channel) == '__uri') { + return false; + } + if ($this->_getChannelFromAlias($channel) == 'pecl.php.net') { + return false; + } + if (!$this->_channelExists($channel)) { + return false; + } + if (!$channel || $this->_getChannelFromAlias($channel) == 'pear.php.net') { + return false; + } + $channel = $this->_getChannelFromAlias($channel); + if ($channel == 'pear.php.net') { + return false; + } + $test = $this->_listChannelPackages($channel); + if (count($test)) { + return false; + } + $test = @rmdir($this->_channelDirectoryName($channel)); + if (!$test) { + return false; + } + $file = $this->_getChannelAliasFileName($this->_getAlias($channel)); + if (file_exists($file)) { + $test = @unlink($file); + if (!$test) { + return false; + } + } + $file = $this->_channelFileName($channel); + $ret = true; + if (file_exists($file)) { + $ret = @unlink($file); + } + return $ret; + } + + // }}} + // {{{ _isChannelAlias() + + /** + * Determine whether a channel exists in the registry + * @param string Channel Alias + * @return boolean + */ + function _isChannelAlias($alias) + { + return file_exists($this->_getChannelAliasFileName($alias)); + } + + // }}} + // {{{ _packageInfo() + + /** + * @param string|null + * @param string|null + * @param string|null + * @return array|null + * @access private + */ + function _packageInfo($package = null, $key = null, $channel = 'pear.php.net') + { + if ($package === null) { + if ($channel === null) { + $channels = $this->_listChannels(); + $ret = array(); + foreach ($channels as $channel) { + $channel = strtolower($channel); + $ret[$channel] = array(); + $packages = $this->_listPackages($channel); + foreach ($packages as $package) { + $ret[$channel][] = $this->_packageInfo($package, null, $channel); + } + } + return $ret; + } + $ps = $this->_listPackages($channel); + if (!count($ps)) { + return array(); + } + return array_map(array(&$this, '_packageInfo'), + $ps, array_fill(0, count($ps), null), + array_fill(0, count($ps), $channel)); + } + $fp = $this->_openPackageFile($package, 'r', $channel); + if ($fp === null) { + return null; + } + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + clearstatcache(); + $this->_closePackageFile($fp); + $data = file_get_contents($this->_packageFileName($package, $channel)); + set_magic_quotes_runtime($rt); + $data = unserialize($data); + if ($key === null) { + return $data; + } + // compatibility for package.xml version 2.0 + if (isset($data['old'][$key])) { + return $data['old'][$key]; + } + if (isset($data[$key])) { + return $data[$key]; + } + return null; + } + + // }}} + // {{{ _channelInfo() + + /** + * @param string Channel name + * @param bool whether to strictly retrieve info of channels, not just aliases + * @return array|null + */ + function _channelInfo($channel, $noaliases = false) + { + if (!$this->_channelExists($channel, $noaliases)) { + return null; + } + $fp = $this->_openChannelFile($channel, 'r'); + if ($fp === null) { + return null; + } + $rt = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + clearstatcache(); + $this->_closeChannelFile($fp); + $data = file_get_contents($this->_channelFileName($channel)); + set_magic_quotes_runtime($rt); + $data = unserialize($data); + return $data; + } + + // }}} + // {{{ _listChannels() + + function _listChannels() + { + $channellist = array(); + if (!file_exists($this->channelsdir) || !is_dir($this->channelsdir)) { + return array('pear.php.net', 'pecl.php.net', '__uri'); + } + $dp = opendir($this->channelsdir); + while ($ent = readdir($dp)) { + if ($ent{0} == '.' || substr($ent, -4) != '.reg') { + continue; + } + if ($ent == '__uri.reg') { + $channellist[] = '__uri'; + continue; + } + $channellist[] = str_replace('_', '/', substr($ent, 0, -4)); + } + closedir($dp); + if (!in_array('pear.php.net', $channellist)) { + $channellist[] = 'pear.php.net'; + } + if (!in_array('pecl.php.net', $channellist)) { + $channellist[] = 'pecl.php.net'; + } + if (!in_array('__uri', $channellist)) { + $channellist[] = '__uri'; + } + + natsort($channellist); + return $channellist; + } + + // }}} + // {{{ _listPackages() + + function _listPackages($channel = false) + { + if ($channel && $this->_getChannelFromAlias($channel) != 'pear.php.net') { + return $this->_listChannelPackages($channel); + } + if (!file_exists($this->statedir) || !is_dir($this->statedir)) { + return array(); + } + $pkglist = array(); + $dp = opendir($this->statedir); + if (!$dp) { + return $pkglist; + } + while ($ent = readdir($dp)) { + if ($ent{0} == '.' || substr($ent, -4) != '.reg') { + continue; + } + $pkglist[] = substr($ent, 0, -4); + } + closedir($dp); + return $pkglist; + } + + // }}} + // {{{ _listChannelPackages() + + function _listChannelPackages($channel) + { + $pkglist = array(); + if (!file_exists($this->_channelDirectoryName($channel)) || + !is_dir($this->_channelDirectoryName($channel))) { + return array(); + } + $dp = opendir($this->_channelDirectoryName($channel)); + if (!$dp) { + return $pkglist; + } + while ($ent = readdir($dp)) { + if ($ent{0} == '.' || substr($ent, -4) != '.reg') { + continue; + } + $pkglist[] = substr($ent, 0, -4); + } + closedir($dp); + return $pkglist; + } + + // }}} + + function _listAllPackages() + { + $ret = array(); + foreach ($this->_listChannels() as $channel) { + $ret[$channel] = $this->_listPackages($channel); + } + return $ret; + } + + /** + * Add an installed package to the registry + * @param string package name + * @param array package info (parsed by PEAR_Common::infoFrom*() methods) + * @return bool success of saving + * @access private + */ + function _addPackage($package, $info) + { + if ($this->_packageExists($package)) { + return false; + } + $fp = $this->_openPackageFile($package, 'wb'); + if ($fp === null) { + return false; + } + $info['_lastmodified'] = time(); + fwrite($fp, serialize($info)); + $this->_closePackageFile($fp); + if (isset($info['filelist'])) { + $this->_rebuildFileMap(); + } + return true; + } + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @return bool + * @access private + */ + function _addPackage2($info) + { + if (!is_a($info, 'PEAR_PackageFile_v1') && !is_a($info, 'PEAR_PackageFile_v2')) { + return false; + } + + if (!$info->validate()) { + if (class_exists('PEAR_Common')) { + $ui = PEAR_Frontend::singleton(); + if ($ui) { + foreach ($info->getValidationWarnings() as $err) { + $ui->log($err['message'], true); + } + } + } + return false; + } + $channel = $info->getChannel(); + $package = $info->getPackage(); + $save = $info; + if ($this->_packageExists($package, $channel)) { + return false; + } + if (!$this->_channelExists($channel, true)) { + return false; + } + $info = $info->toArray(true); + if (!$info) { + return false; + } + $fp = $this->_openPackageFile($package, 'wb', $channel); + if ($fp === null) { + return false; + } + $info['_lastmodified'] = time(); + fwrite($fp, serialize($info)); + $this->_closePackageFile($fp); + $this->_rebuildFileMap(); + return true; + } + + /** + * @param string Package name + * @param array parsed package.xml 1.0 + * @param bool this parameter is only here for BC. Don't use it. + * @access private + */ + function _updatePackage($package, $info, $merge = true) + { + $oldinfo = $this->_packageInfo($package); + if (empty($oldinfo)) { + return false; + } + $fp = $this->_openPackageFile($package, 'w'); + if ($fp === null) { + return false; + } + if (is_object($info)) { + $info = $info->toArray(); + } + $info['_lastmodified'] = time(); + $newinfo = $info; + if ($merge) { + $info = array_merge($oldinfo, $info); + } else { + $diff = $info; + } + fwrite($fp, serialize($info)); + $this->_closePackageFile($fp); + if (isset($newinfo['filelist'])) { + $this->_rebuildFileMap(); + } + return true; + } + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @return bool + * @access private + */ + function _updatePackage2($info) + { + if (!$this->_packageExists($info->getPackage(), $info->getChannel())) { + return false; + } + $fp = $this->_openPackageFile($info->getPackage(), 'w', $info->getChannel()); + if ($fp === null) { + return false; + } + $save = $info; + $info = $save->getArray(true); + $info['_lastmodified'] = time(); + fwrite($fp, serialize($info)); + $this->_closePackageFile($fp); + $this->_rebuildFileMap(); + return true; + } + + /** + * @param string Package name + * @param string Channel name + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2|null + * @access private + */ + function &_getPackage($package, $channel = 'pear.php.net') + { + $info = $this->_packageInfo($package, null, $channel); + if ($info === null) { + return $info; + } + $a = $this->_config; + if (!$a) { + $this->_config = &new PEAR_Config; + $this->_config->set('php_dir', $this->statedir); + } + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + $pkg = &new PEAR_PackageFile($this->_config); + $pf = &$pkg->fromArray($info); + return $pf; + } + + /** + * @param string channel name + * @param bool whether to strictly retrieve channel names + * @return PEAR_ChannelFile|PEAR_Error + * @access private + */ + function &_getChannel($channel, $noaliases = false) + { + $ch = false; + if ($this->_channelExists($channel, $noaliases)) { + $chinfo = $this->_channelInfo($channel, $noaliases); + if ($chinfo) { + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $ch = &PEAR_ChannelFile::fromArrayWithErrors($chinfo); + } + } + if ($ch) { + if ($ch->validate()) { + return $ch; + } + foreach ($ch->getErrors(true) as $err) { + $message = $err['message'] . "\n"; + } + $ch = PEAR::raiseError($message); + return $ch; + } + if ($this->_getChannelFromAlias($channel) == 'pear.php.net') { + // the registry is not properly set up, so use defaults + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $pear_channel = new PEAR_ChannelFile; + $pear_channel->setName('pear.php.net'); + $pear_channel->setAlias('pear'); + $pear_channel->setSummary('PHP Extension and Application Repository'); + $pear_channel->setDefaultPEARProtocols(); + $pear_channel->setBaseURL('REST1.0', 'http://pear.php.net/rest/'); + $pear_channel->setBaseURL('REST1.1', 'http://pear.php.net/rest/'); + return $pear_channel; + } + if ($this->_getChannelFromAlias($channel) == 'pecl.php.net') { + // the registry is not properly set up, so use defaults + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $pear_channel = new PEAR_ChannelFile; + $pear_channel->setName('pecl.php.net'); + $pear_channel->setAlias('pecl'); + $pear_channel->setSummary('PHP Extension Community Library'); + $pear_channel->setDefaultPEARProtocols(); + $pear_channel->setBaseURL('REST1.0', 'http://pecl.php.net/rest/'); + $pear_channel->setBaseURL('REST1.1', 'http://pecl.php.net/rest/'); + $pear_channel->setValidationPackage('PEAR_Validator_PECL', '1.0'); + return $pear_channel; + } + if ($this->_getChannelFromAlias($channel) == '__uri') { + // the registry is not properly set up, so use defaults + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + $private = new PEAR_ChannelFile; + $private->setName('__uri'); + $private->addFunction('xmlrpc', '1.0', '****'); + $private->setSummary('Pseudo-channel for static packages'); + return $private; + } + return $ch; + } + + // {{{ packageExists() + + /** + * @param string Package name + * @param string Channel name + * @return bool + */ + function packageExists($package, $channel = 'pear.php.net') + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_packageExists($package, $channel); + $this->_unlock(); + return $ret; + } + + // }}} + + // {{{ channelExists() + + /** + * @param string channel name + * @param bool if true, then aliases will be ignored + * @return bool + */ + function channelExists($channel, $noaliases = false) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_channelExists($channel, $noaliases); + $this->_unlock(); + return $ret; + } + + // }}} + + // {{{ isAlias() + + /** + * Determines whether the parameter is an alias of a channel + * @param string + * @return bool + */ + function isAlias($alias) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_isChannelAlias($alias); + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ packageInfo() + + /** + * @param string|null + * @param string|null + * @param string + * @return array|null + */ + function packageInfo($package = null, $key = null, $channel = 'pear.php.net') + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_packageInfo($package, $key, $channel); + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ channelInfo() + + /** + * Retrieve a raw array of channel data. + * + * Do not use this, instead use {@link getChannel()} for normal + * operations. Array structure is undefined in this method + * @param string channel name + * @param bool whether to strictly retrieve information only on non-aliases + * @return array|null|PEAR_Error + */ + function channelInfo($channel = null, $noaliases = false) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_channelInfo($channel, $noaliases); + $this->_unlock(); + return $ret; + } + + // }}} + + /** + * @param string + */ + function channelName($channel) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_getChannelFromAlias($channel); + $this->_unlock(); + return $ret; + } + + /** + * @param string + */ + function channelAlias($channel) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_getAlias($channel); + $this->_unlock(); + return $ret; + } + // {{{ listPackages() + + function listPackages($channel = false) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_listPackages($channel); + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ listAllPackages() + + function listAllPackages() + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_listAllPackages(); + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ listChannel() + + function listChannels() + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = $this->_listChannels(); + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ addPackage() + + /** + * Add an installed package to the registry + * @param string|PEAR_PackageFile_v1|PEAR_PackageFile_v2 package name or object + * that will be passed to {@link addPackage2()} + * @param array package info (parsed by PEAR_Common::infoFrom*() methods) + * @return bool success of saving + */ + function addPackage($package, $info) + { + if (is_object($info)) { + return $this->addPackage2($info); + } + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_addPackage($package, $info); + $this->_unlock(); + if ($ret) { + if (!class_exists('PEAR_PackageFile_v1')) { + require_once 'PEAR/PackageFile/v1.php'; + } + $pf = new PEAR_PackageFile_v1; + $pf->setConfig($this->_config); + $pf->fromArray($info); + $this->_dependencyDB->uninstallPackage($pf); + $this->_dependencyDB->installPackage($pf); + } + return $ret; + } + + // }}} + // {{{ addPackage2() + + function addPackage2($info) + { + if (!is_object($info)) { + return $this->addPackage($info['package'], $info); + } + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_addPackage2($info); + $this->_unlock(); + if ($ret) { + $this->_dependencyDB->uninstallPackage($info); + $this->_dependencyDB->installPackage($info); + } + return $ret; + } + + // }}} + // {{{ updateChannel() + + /** + * For future expandibility purposes, separate this + * @param PEAR_ChannelFile + */ + function updateChannel($channel, $lastmodified = null) + { + if ($channel->getName() == '__uri') { + return false; + } + return $this->addChannel($channel, $lastmodified, true); + } + + // }}} + // {{{ deleteChannel() + + /** + * Deletion fails if there are any packages installed from the channel + * @param string|PEAR_ChannelFile channel name + * @return boolean|PEAR_Error True on deletion, false if it doesn't exist + */ + function deleteChannel($channel) + { + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_deleteChannel($channel); + $this->_unlock(); + if ($ret && is_a($this->_config, 'PEAR_Config')) { + $this->_config->setChannels($this->listChannels()); + } + return $ret; + } + + // }}} + // {{{ addChannel() + + /** + * @param PEAR_ChannelFile Channel object + * @param string Last-Modified header from HTTP for caching + * @return boolean|PEAR_Error True on creation, false if it already exists + */ + function addChannel($channel, $lastmodified = false, $update = false) + { + if (!is_a($channel, 'PEAR_ChannelFile')) { + return false; + } + if (!$channel->validate()) { + return false; + } + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_addChannel($channel, $update, $lastmodified); + $this->_unlock(); + if (!$update && $ret && is_a($this->_config, 'PEAR_Config')) { + $this->_config->setChannels($this->listChannels()); + } + return $ret; + } + + // }}} + // {{{ deletePackage() + + function deletePackage($package, $channel = 'pear.php.net') + { + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $file = $this->_packageFileName($package, $channel); + if (file_exists($file)) { + $ret = @unlink($file); + } else { + $ret = false; + } + $this->_rebuildFileMap(); + $this->_unlock(); + $p = array('channel' => $channel, 'package' => $package); + $this->_dependencyDB->uninstallPackage($p); + return $ret; + } + + // }}} + // {{{ updatePackage() + + function updatePackage($package, $info, $merge = true) + { + if (is_object($info)) { + return $this->updatePackage2($info, $merge); + } + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_updatePackage($package, $info, $merge); + $this->_unlock(); + if ($ret) { + if (!class_exists('PEAR_PackageFile_v1')) { + require_once 'PEAR/PackageFile/v1.php'; + } + $pf = new PEAR_PackageFile_v1; + $pf->setConfig($this->_config); + $pf->fromArray($this->packageInfo($package)); + $this->_dependencyDB->uninstallPackage($pf); + $this->_dependencyDB->installPackage($pf); + } + return $ret; + } + + // }}} + // {{{ updatePackage2() + + function updatePackage2($info) + { + if (!is_object($info)) { + return $this->updatePackage($info['package'], $info, $merge); + } + if (!$info->validate(PEAR_VALIDATE_DOWNLOADING)) { + return false; + } + if (PEAR::isError($e = $this->_lock(LOCK_EX))) { + return $e; + } + $ret = $this->_updatePackage2($info); + $this->_unlock(); + if ($ret) { + $this->_dependencyDB->uninstallPackage($info); + $this->_dependencyDB->installPackage($info); + } + return $ret; + } + + // }}} + // {{{ getChannel() + /** + * @param string channel name + * @param bool whether to strictly return raw channels (no aliases) + * @return PEAR_ChannelFile|PEAR_Error + */ + function &getChannel($channel, $noaliases = false) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $ret = &$this->_getChannel($channel, $noaliases); + if (!$ret) { + return PEAR::raiseError('Unknown channel: ' . $channel); + } + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ getPackage() + /** + * @param string package name + * @param string channel name + * @return PEAR_PackageFile_v1|PEAR_PackageFile_v2|null + */ + function &getPackage($package, $channel = 'pear.php.net') + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $pf = &$this->_getPackage($package, $channel); + $this->_unlock(); + return $pf; + } + + // }}} + + /** + * Get PEAR_PackageFile_v[1/2] objects representing the contents of + * a dependency group that are installed. + * + * This is used at uninstall-time + * @param array + * @return array|false + */ + function getInstalledGroup($group) + { + $ret = array(); + if (isset($group['package'])) { + if (!isset($group['package'][0])) { + $group['package'] = array($group['package']); + } + foreach ($group['package'] as $package) { + $depchannel = isset($package['channel']) ? $package['channel'] : '__uri'; + $p = &$this->getPackage($package['name'], $depchannel); + if ($p) { + $save = &$p; + $ret[] = &$save; + } + } + } + if (isset($group['subpackage'])) { + if (!isset($group['subpackage'][0])) { + $group['subpackage'] = array($group['subpackage']); + } + foreach ($group['subpackage'] as $package) { + $depchannel = isset($package['channel']) ? $package['channel'] : '__uri'; + $p = &$this->getPackage($package['name'], $depchannel); + if ($p) { + $save = &$p; + $ret[] = &$save; + } + } + } + if (!count($ret)) { + return false; + } + return $ret; + } + + // {{{ getChannelValidator() + /** + * @param string channel name + * @return PEAR_Validate|false + */ + function &getChannelValidator($channel) + { + $chan = $this->getChannel($channel); + if (PEAR::isError($chan)) { + return $chan; + } + $val = $chan->getValidationObject(); + return $val; + } + // }}} + // {{{ getChannels() + /** + * @param string channel name + * @return array an array of PEAR_ChannelFile objects representing every installed channel + */ + function &getChannels() + { + $ret = array(); + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + foreach ($this->_listChannels() as $channel) { + $e = &$this->_getChannel($channel); + if (!$e || PEAR::isError($e)) { + continue; + } + $ret[] = $e; + } + $this->_unlock(); + return $ret; + } + + // }}} + // {{{ checkFileMap() + + /** + * Test whether a file or set of files belongs to a package. + * + * If an array is passed in + * @param string|array file path, absolute or relative to the pear + * install dir + * @param string|array name of PEAR package or array('package' => name, 'channel' => + * channel) of a package that will be ignored + * @param string API version - 1.1 will exclude any files belonging to a package + * @param array private recursion variable + * @return array|false which package and channel the file belongs to, or an empty + * string if the file does not belong to an installed package, + * or belongs to the second parameter's package + */ + function checkFileMap($path, $package = false, $api = '1.0', $attrs = false) + { + if (is_array($path)) { + static $notempty; + if (empty($notempty)) { + if (!class_exists('PEAR_Installer_Role')) { + require_once 'PEAR/Installer/Role.php'; + } + $notempty = create_function('$a','return !empty($a);'); + } + $package = is_array($package) ? array(strtolower($package[0]), strtolower($package[1])) + : strtolower($package); + $pkgs = array(); + foreach ($path as $name => $attrs) { + if (is_array($attrs)) { + if (isset($attrs['install-as'])) { + $name = $attrs['install-as']; + } + if (!in_array($attrs['role'], PEAR_Installer_Role::getInstallableRoles())) { + // these are not installed + continue; + } + if (!in_array($attrs['role'], PEAR_Installer_Role::getBaseinstallRoles())) { + $attrs['baseinstalldir'] = is_array($package) ? $package[1] : $package; + } + if (isset($attrs['baseinstalldir'])) { + $name = $attrs['baseinstalldir'] . DIRECTORY_SEPARATOR . $name; + } + } + $pkgs[$name] = $this->checkFileMap($name, $package, $api, $attrs); + if (PEAR::isError($pkgs[$name])) { + return $pkgs[$name]; + } + } + return array_filter($pkgs, $notempty); + } + if (empty($this->filemap_cache)) { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + $err = $this->_readFileMap(); + $this->_unlock(); + if (PEAR::isError($err)) { + return $err; + } + } + if (!$attrs) { + $attrs = array('role' => 'php'); // any old call would be for PHP role only + } + if (isset($this->filemap_cache[$attrs['role']][$path])) { + if ($api >= '1.1' && $this->filemap_cache[$attrs['role']][$path] == $package) { + return false; + } + return $this->filemap_cache[$attrs['role']][$path]; + } + $l = strlen($this->install_dir); + if (substr($path, 0, $l) == $this->install_dir) { + $path = preg_replace('!^'.DIRECTORY_SEPARATOR.'+!', '', substr($path, $l)); + } + if (isset($this->filemap_cache[$attrs['role']][$path])) { + if ($api >= '1.1' && $this->filemap_cache[$attrs['role']][$path] == $package) { + return false; + } + return $this->filemap_cache[$attrs['role']][$path]; + } + return false; + } + + // }}} + // {{{ flush() + /** + * Force a reload of the filemap + * @since 1.5.0RC3 + */ + function flushFileMap() + { + $this->filemap_cache = null; + clearstatcache(); // ensure that the next read gets the full, current filemap + } + + // }}} + // {{{ apiVersion() + /** + * Get the expected API version. Channels API is version 1.1, as it is backwards + * compatible with 1.0 + * @return string + */ + function apiVersion() + { + return '1.1'; + } + // }}} + + + /** + * Parse a package name, or validate a parsed package name array + * @param string|array pass in an array of format + * array( + * 'package' => 'pname', + * ['channel' => 'channame',] + * ['version' => 'version',] + * ['state' => 'state',] + * ['group' => 'groupname']) + * or a string of format + * [channel://][channame/]pname[-version|-state][/group=groupname] + * @return array|PEAR_Error + */ + function parsePackageName($param, $defaultchannel = 'pear.php.net') + { + $saveparam = $param; + if (is_array($param)) { + // convert to string for error messages + $saveparam = $this->parsedPackageNameToString($param); + // process the array + if (!isset($param['package'])) { + return PEAR::raiseError('parsePackageName(): array $param ' . + 'must contain a valid package name in index "param"', + 'package', null, null, $param); + } + if (!isset($param['uri'])) { + if (!isset($param['channel'])) { + $param['channel'] = $defaultchannel; + } + } else { + $param['channel'] = '__uri'; + } + } else { + $components = @parse_url((string) $param); + if (isset($components['scheme'])) { + if ($components['scheme'] == 'http') { + // uri package + $param = array('uri' => $param, 'channel' => '__uri'); + } elseif($components['scheme'] != 'channel') { + return PEAR::raiseError('parsePackageName(): only channel:// uris may ' . + 'be downloaded, not "' . $param . '"', 'invalid', null, null, $param); + } + } + if (!isset($components['path'])) { + return PEAR::raiseError('parsePackageName(): array $param ' . + 'must contain a valid package name in "' . $param . '"', + 'package', null, null, $param); + } + if (isset($components['host'])) { + // remove the leading "/" + $components['path'] = substr($components['path'], 1); + } + if (!isset($components['scheme'])) { + if (strpos($components['path'], '/') !== false) { + if ($components['path']{0} == '/') { + return PEAR::raiseError('parsePackageName(): this is not ' . + 'a package name, it begins with "/" in "' . $param . '"', + 'invalid', null, null, $param); + } + $parts = explode('/', $components['path']); + $components['host'] = array_shift($parts); + if (count($parts) > 1) { + $components['path'] = array_pop($parts); + $components['host'] .= '/' . implode('/', $parts); + } else { + $components['path'] = implode('/', $parts); + } + } else { + $components['host'] = $defaultchannel; + } + } else { + if (strpos($components['path'], '/')) { + $parts = explode('/', $components['path']); + $components['path'] = array_pop($parts); + $components['host'] .= '/' . implode('/', $parts); + } + } + + if (is_array($param)) { + $param['package'] = $components['path']; + } else { + $param = array( + 'package' => $components['path'] + ); + if (isset($components['host'])) { + $param['channel'] = $components['host']; + } + } + if (isset($components['fragment'])) { + $param['group'] = $components['fragment']; + } + if (isset($components['user'])) { + $param['user'] = $components['user']; + } + if (isset($components['pass'])) { + $param['pass'] = $components['pass']; + } + if (isset($components['query'])) { + parse_str($components['query'], $param['opts']); + } + // check for extension + $pathinfo = pathinfo($param['package']); + if (isset($pathinfo['extension']) && + in_array(strtolower($pathinfo['extension']), array('tgz', 'tar'))) { + $param['extension'] = $pathinfo['extension']; + $param['package'] = substr($pathinfo['basename'], 0, + strlen($pathinfo['basename']) - 4); + } + // check for version + if (strpos($param['package'], '-')) { + $test = explode('-', $param['package']); + if (count($test) != 2) { + return PEAR::raiseError('parsePackageName(): only one version/state ' . + 'delimiter "-" is allowed in "' . $saveparam . '"', + 'version', null, null, $param); + } + list($param['package'], $param['version']) = $test; + } + } + // validation + $info = $this->channelExists($param['channel']); + if (PEAR::isError($info)) { + return $info; + } + if (!$info) { + return PEAR::raiseError('unknown channel "' . $param['channel'] . + '" in "' . $saveparam . '"', 'channel', null, null, $param); + } + $chan = $this->getChannel($param['channel']); + if (PEAR::isError($chan)) { + return $chan; + } + if (!$chan) { + return PEAR::raiseError("Exception: corrupt registry, could not " . + "retrieve channel " . $param['channel'] . " information", + 'registry', null, null, $param); + } + $param['channel'] = $chan->getName(); + $validate = $chan->getValidationObject(); + $vpackage = $chan->getValidationPackage(); + // validate package name + if (!$validate->validPackageName($param['package'], $vpackage['_content'])) { + return PEAR::raiseError('parsePackageName(): invalid package name "' . + $param['package'] . '" in "' . $saveparam . '"', + 'package', null, null, $param); + } + if (isset($param['group'])) { + if (!PEAR_Validate::validGroupName($param['group'])) { + return PEAR::raiseError('parsePackageName(): dependency group "' . $param['group'] . + '" is not a valid group name in "' . $saveparam . '"', 'group', null, null, + $param); + } + } + if (isset($param['state'])) { + if (!in_array(strtolower($param['state']), $validate->getValidStates())) { + return PEAR::raiseError('parsePackageName(): state "' . $param['state'] + . '" is not a valid state in "' . $saveparam . '"', + 'state', null, null, $param); + } + } + if (isset($param['version'])) { + if (isset($param['state'])) { + return PEAR::raiseError('parsePackageName(): cannot contain both ' . + 'a version and a stability (state) in "' . $saveparam . '"', + 'version/state', null, null, $param); + } + // check whether version is actually a state + if (in_array(strtolower($param['version']), $validate->getValidStates())) { + $param['state'] = strtolower($param['version']); + unset($param['version']); + } else { + if (!$validate->validVersion($param['version'])) { + return PEAR::raiseError('parsePackageName(): "' . $param['version'] . + '" is neither a valid version nor a valid state in "' . + $saveparam . '"', 'version/state', null, null, $param); + } + } + } + return $param; + } + + /** + * @param array + * @return string + */ + function parsedPackageNameToString($parsed, $brief = false) + { + if (is_string($parsed)) { + return $parsed; + } + if (is_object($parsed)) { + $p = $parsed; + $parsed = array( + 'package' => $p->getPackage(), + 'channel' => $p->getChannel(), + 'version' => $p->getVersion(), + ); + } + if (isset($parsed['uri'])) { + return $parsed['uri']; + } + if ($brief) { + if ($channel = $this->channelAlias($parsed['channel'])) { + return $channel . '/' . $parsed['package']; + } + } + $upass = ''; + if (isset($parsed['user'])) { + $upass = $parsed['user']; + if (isset($parsed['pass'])) { + $upass .= ':' . $parsed['pass']; + } + $upass = "$upass@"; + } + $ret = 'channel://' . $upass . $parsed['channel'] . '/' . $parsed['package']; + if (isset($parsed['version']) || isset($parsed['state'])) { + $ver = isset($parsed['version']) ? $parsed['version'] : ''; + $ver .= isset($parsed['state']) ? $parsed['state'] : ''; + $ret .= '-' . $ver; + } + if (isset($parsed['extension'])) { + $ret .= '.' . $parsed['extension']; + } + if (isset($parsed['opts'])) { + $ret .= '?'; + foreach ($parsed['opts'] as $name => $value) { + $parsed['opts'][$name] = "$name=$value"; + } + $ret .= implode('&', $parsed['opts']); + } + if (isset($parsed['group'])) { + $ret .= '#' . $parsed['group']; + } + return $ret; + } +} + +?> diff --git a/PEAR/Remote.php b/PEAR/Remote.php new file mode 100644 index 0000000..bd7cee8 --- /dev/null +++ b/PEAR/Remote.php @@ -0,0 +1,498 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Remote.php,v 1.79 2006/03/27 04:33:11 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * needed for PEAR_Error + */ +require_once 'PEAR.php'; +require_once 'PEAR/Config.php'; + +/** + * This is a class for doing remote operations against the central + * PEAR database. + * + * @nodep XML_RPC_Value + * @nodep XML_RPC_Message + * @nodep XML_RPC_Client + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Remote extends PEAR +{ + // {{{ properties + + var $config = null; + var $cache = null; + /** + * @var PEAR_Registry + * @access private + */ + var $_registry; + + // }}} + + // {{{ PEAR_Remote(config_object) + + function PEAR_Remote(&$config) + { + $this->PEAR(); + $this->config = &$config; + $this->_registry = &$this->config->getRegistry(); + } + + // }}} + // {{{ setRegistry() + + function setRegistry(&$reg) + { + $this->_registry = &$reg; + } + // }}} + // {{{ getCache() + + + function getCache($args) + { + $id = md5(serialize($args)); + $cachedir = $this->config->get('cache_dir'); + $filename = $cachedir . DIRECTORY_SEPARATOR . 'xmlrpc_cache_' . $id; + if (!file_exists($filename)) { + return null; + } + + $fp = fopen($filename, 'rb'); + if (!$fp) { + return null; + } + fclose($fp); + $content = file_get_contents($filename); + $result = array( + 'age' => time() - filemtime($filename), + 'lastChange' => filemtime($filename), + 'content' => unserialize($content), + ); + return $result; + } + + // }}} + + // {{{ saveCache() + + function saveCache($args, $data) + { + $id = md5(serialize($args)); + $cachedir = $this->config->get('cache_dir'); + if (!file_exists($cachedir)) { + System::mkdir(array('-p', $cachedir)); + } + $filename = $cachedir.'/xmlrpc_cache_'.$id; + + $fp = @fopen($filename, "wb"); + if ($fp) { + fwrite($fp, serialize($data)); + fclose($fp); + } + } + + // }}} + + // {{{ clearCache() + + function clearCache($method, $args) + { + array_unshift($args, $method); + array_unshift($args, $this->config->get('default_channel')); // cache by channel + $id = md5(serialize($args)); + $cachedir = $this->config->get('cache_dir'); + $filename = $cachedir.'/xmlrpc_cache_'.$id; + if (file_exists($filename)) { + @unlink($filename); + } + } + + // }}} + // {{{ call(method, [args...]) + + function call($method) + { + $_args = $args = func_get_args(); + + $server_channel = $this->config->get('default_channel'); + $channel = $this->_registry->getChannel($server_channel); + if (!PEAR::isError($channel)) { + $mirror = $this->config->get('preferred_mirror'); + if ($channel->getMirror($mirror)) { + if ($channel->supports('xmlrpc', $method, $mirror)) { + $server_channel = $server_host = $mirror; // use the preferred mirror + $server_port = $channel->getPort($mirror); + } elseif (!$channel->supports('xmlrpc', $method)) { + return $this->raiseError("Channel $server_channel does not " . + "support xml-rpc method $method"); + } + } + if (!isset($server_host)) { + if (!$channel->supports('xmlrpc', $method)) { + return $this->raiseError("Channel $server_channel does not support " . + "xml-rpc method $method"); + } else { + $server_host = $server_channel; + $server_port = $channel->getPort(); + } + } + } else { + return $this->raiseError("Unknown channel '$server_channel'"); + } + + array_unshift($_args, $server_channel); // cache by channel + $this->cache = $this->getCache($_args); + $cachettl = $this->config->get('cache_ttl'); + // If cache is newer than $cachettl seconds, we use the cache! + if ($this->cache !== null && $this->cache['age'] < $cachettl) { + return $this->cache['content']; + } + $fp = false; + if (extension_loaded("xmlrpc")) { + $result = call_user_func_array(array(&$this, 'call_epi'), $args); + if (!PEAR::isError($result)) { + $this->saveCache($_args, $result); + } + return $result; + } elseif (!($fp = fopen('XML/RPC.php', 'r', true))) { + return $this->raiseError("For this remote PEAR operation you need to load the xmlrpc extension or install XML_RPC"); + } + include_once 'XML/RPC.php'; + if ($fp) { + fclose($fp); + } + + array_shift($args); + $username = $this->config->get('username'); + $password = $this->config->get('password'); + $eargs = array(); + foreach($args as $arg) { + $eargs[] = $this->_encode($arg); + } + $f = new XML_RPC_Message($method, $eargs); + if ($this->cache !== null) { + $maxAge = '?maxAge='.$this->cache['lastChange']; + } else { + $maxAge = ''; + } + $proxy_host = $proxy_port = $proxy_user = $proxy_pass = ''; + if ($proxy = parse_url($this->config->get('http_proxy'))) { + $proxy_host = isset($proxy['host']) ? $proxy['host'] : null; + if (isset($proxy['scheme']) && $proxy['scheme'] == 'https') { + $proxy_host = 'https://' . $proxy_host; + } + $proxy_port = isset($proxy['port']) ? $proxy['port'] : 8080; + $proxy_user = isset($proxy['user']) ? urldecode($proxy['user']) : null; + $proxy_pass = isset($proxy['pass']) ? urldecode($proxy['pass']) : null; + } + $shost = $server_host; + if ($channel->getSSL()) { + $shost = "https://$shost"; + } + $c = new XML_RPC_Client('/' . $channel->getPath('xmlrpc') + . $maxAge, $shost, $server_port, $proxy_host, $proxy_port, + $proxy_user, $proxy_pass); + if ($username && $password) { + $c->setCredentials($username, $password); + } + if ($this->config->get('verbose') >= 3) { + $c->setDebug(1); + } + $r = $c->send($f); + if (!$r) { + return $this->raiseError("XML_RPC send failed"); + } + $v = $r->value(); + if ($e = $r->faultCode()) { + if ($e == $GLOBALS['XML_RPC_err']['http_error'] && strstr($r->faultString(), '304 Not Modified') !== false) { + return $this->cache['content']; + } + return $this->raiseError($r->faultString(), $e); + } + + $result = XML_RPC_decode($v); + $this->saveCache($_args, $result); + return $result; + } + + // }}} + + // {{{ call_epi(method, [args...]) + + function call_epi($method) + { + if (!extension_loaded("xmlrpc")) { + return $this->raiseError("xmlrpc extension is not loaded"); + } + $server_channel = $this->config->get('default_channel'); + $channel = $this->_registry->getChannel($server_channel); + if (!PEAR::isError($channel)) { + $mirror = $this->config->get('preferred_mirror'); + if ($channel->getMirror($mirror)) { + if ($channel->supports('xmlrpc', $method, $mirror)) { + $server_channel = $server_host = $mirror; // use the preferred mirror + $server_port = $channel->getPort($mirror); + } elseif (!$channel->supports('xmlrpc', $method)) { + return $this->raiseError("Channel $server_channel does not " . + "support xml-rpc method $method"); + } + } + if (!isset($server_host)) { + if (!$channel->supports('xmlrpc', $method)) { + return $this->raiseError("Channel $server_channel does not support " . + "xml-rpc method $method"); + } else { + $server_host = $server_channel; + $server_port = $channel->getPort(); + } + } + } else { + return $this->raiseError("Unknown channel '$server_channel'"); + } + $params = func_get_args(); + array_shift($params); + $method = str_replace("_", ".", $method); + $request = xmlrpc_encode_request($method, $params); + if ($http_proxy = $this->config->get('http_proxy')) { + $proxy = parse_url($http_proxy); + $proxy_host = $proxy_port = $proxy_user = $proxy_pass = ''; + $proxy_host = isset($proxy['host']) ? $proxy['host'] : null; + if (isset($proxy['scheme']) && $proxy['scheme'] == 'https') { + $proxy_host = 'https://' . $proxy_host; + } + $proxy_port = isset($proxy['port']) ? $proxy['port'] : null; + $proxy_user = isset($proxy['user']) ? urldecode($proxy['user']) : null; + $proxy_pass = isset($proxy['pass']) ? urldecode($proxy['pass']) : null; + $fp = @fsockopen($proxy_host, $proxy_port); + $use_proxy = true; + if ($channel->getSSL()) { + $server_host = "https://$server_host"; + } + } else { + $use_proxy = false; + $ssl = $channel->getSSL(); + $fp = @fsockopen(($ssl ? 'ssl://' : '') . $server_host, $server_port); + if (!$fp) { + $server_host = "$ssl$server_host"; // for error-reporting + } + } + if (!$fp && $http_proxy) { + return $this->raiseError("PEAR_Remote::call: fsockopen(`$proxy_host', $proxy_port) failed"); + } elseif (!$fp) { + return $this->raiseError("PEAR_Remote::call: fsockopen(`$server_host', $server_port) failed"); + } + $len = strlen($request); + $req_headers = "Host: $server_host:$server_port\r\n" . + "Content-type: text/xml\r\n" . + "Content-length: $len\r\n"; + $username = $this->config->get('username'); + $password = $this->config->get('password'); + if ($username && $password) { + $req_headers .= "Cookie: PEAR_USER=$username; PEAR_PW=$password\r\n"; + $tmp = base64_encode("$username:$password"); + $req_headers .= "Authorization: Basic $tmp\r\n"; + } + if ($this->cache !== null) { + $maxAge = '?maxAge='.$this->cache['lastChange']; + } else { + $maxAge = ''; + } + + if ($use_proxy && $proxy_host != '' && $proxy_user != '') { + $req_headers .= 'Proxy-Authorization: Basic ' + .base64_encode($proxy_user.':'.$proxy_pass) + ."\r\n"; + } + + if ($this->config->get('verbose') > 3) { + print "XMLRPC REQUEST HEADERS:\n"; + var_dump($req_headers); + print "XMLRPC REQUEST BODY:\n"; + var_dump($request); + } + + if ($use_proxy && $proxy_host != '') { + $post_string = "POST http://".$server_host; + if ($proxy_port > '') { + $post_string .= ':'.$server_port; + } + } else { + $post_string = "POST "; + } + + $path = '/' . $channel->getPath('xmlrpc'); + fwrite($fp, ($post_string . $path . "$maxAge HTTP/1.0\r\n$req_headers\r\n$request")); + $response = ''; + $line1 = fgets($fp, 2048); + if (!preg_match('!^HTTP/[0-9\.]+ (\d+) (.*)!', $line1, $matches)) { + return $this->raiseError("PEAR_Remote: invalid HTTP response from XML-RPC server"); + } + switch ($matches[1]) { + case "200": // OK + break; + case "304": // Not Modified + return $this->cache['content']; + case "401": // Unauthorized + if ($username && $password) { + return $this->raiseError("PEAR_Remote ($server_host:$server_port) " . + ": authorization failed", 401); + } else { + return $this->raiseError("PEAR_Remote ($server_host:$server_port) " . + ": authorization required, please log in first", 401); + } + default: + return $this->raiseError("PEAR_Remote ($server_host:$server_port) : " . + "unexpected HTTP response", (int)$matches[1], null, null, + "$matches[1] $matches[2]"); + } + while (trim(fgets($fp, 2048)) != ''); // skip rest of headers + while ($chunk = fread($fp, 10240)) { + $response .= $chunk; + } + fclose($fp); + if ($this->config->get('verbose') > 3) { + print "XMLRPC RESPONSE:\n"; + var_dump($response); + } + $ret = xmlrpc_decode($response); + if (is_array($ret) && isset($ret['__PEAR_TYPE__'])) { + if ($ret['__PEAR_TYPE__'] == 'error') { + if (isset($ret['__PEAR_CLASS__'])) { + $class = $ret['__PEAR_CLASS__']; + } else { + $class = "PEAR_Error"; + } + if ($ret['code'] === '') $ret['code'] = null; + if ($ret['message'] === '') $ret['message'] = null; + if ($ret['userinfo'] === '') $ret['userinfo'] = null; + if (strtolower($class) == 'db_error') { + $ret = $this->raiseError(PEAR::errorMessage($ret['code']), + $ret['code'], null, null, + $ret['userinfo']); + } else { + $ret = $this->raiseError($ret['message'], $ret['code'], + null, null, $ret['userinfo']); + } + } + } elseif (is_array($ret) && sizeof($ret) == 1 && isset($ret[0]) + && is_array($ret[0]) && + !empty($ret[0]['faultString']) && + !empty($ret[0]['faultCode'])) { + extract($ret[0]); + $faultString = "XML-RPC Server Fault: " . + str_replace("\n", " ", $faultString); + return $this->raiseError($faultString, $faultCode); + } elseif (is_array($ret) && sizeof($ret) == 2 && !empty($ret['faultString']) && + !empty($ret['faultCode'])) { + extract($ret); + $faultString = "XML-RPC Server Fault: " . + str_replace("\n", " ", $faultString); + return $this->raiseError($faultString, $faultCode); + } + return $ret; + } + + // }}} + + // {{{ _encode + + // a slightly extended version of XML_RPC_encode + function _encode($php_val) + { + global $XML_RPC_Boolean, $XML_RPC_Int, $XML_RPC_Double; + global $XML_RPC_String, $XML_RPC_Array, $XML_RPC_Struct; + + $type = gettype($php_val); + $xmlrpcval = new XML_RPC_Value; + + switch($type) { + case "array": + reset($php_val); + $firstkey = key($php_val); + end($php_val); + $lastkey = key($php_val); + reset($php_val); + if ($firstkey === 0 && is_int($lastkey) && + ($lastkey + 1) == count($php_val)) { + $is_continuous = true; + reset($php_val); + $size = count($php_val); + for ($expect = 0; $expect < $size; $expect++, next($php_val)) { + if (key($php_val) !== $expect) { + $is_continuous = false; + break; + } + } + if ($is_continuous) { + reset($php_val); + $arr = array(); + while (list($k, $v) = each($php_val)) { + $arr[$k] = $this->_encode($v); + } + $xmlrpcval->addArray($arr); + break; + } + } + // fall though if not numerical and continuous + case "object": + $arr = array(); + while (list($k, $v) = each($php_val)) { + $arr[$k] = $this->_encode($v); + } + $xmlrpcval->addStruct($arr); + break; + case "integer": + $xmlrpcval->addScalar($php_val, $XML_RPC_Int); + break; + case "double": + $xmlrpcval->addScalar($php_val, $XML_RPC_Double); + break; + case "string": + case "NULL": + $xmlrpcval->addScalar($php_val, $XML_RPC_String); + break; + case "boolean": + $xmlrpcval->addScalar($php_val, $XML_RPC_Boolean); + break; + case "unknown type": + default: + return null; + } + return $xmlrpcval; + } + + // }}} + +} + +?> diff --git a/PEAR/RunTest.php b/PEAR/RunTest.php new file mode 100644 index 0000000..584e0b9 --- /dev/null +++ b/PEAR/RunTest.php @@ -0,0 +1,848 @@ + + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: RunTest.php,v 1.45 2007/06/19 03:10:49 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.3.3 + */ + +/** + * for error handling + */ +require_once 'PEAR.php'; +require_once 'PEAR/Config.php'; + +define('DETAILED', 1); +putenv("PHP_PEAR_RUNTESTS=1"); + +/** + * Simplified version of PHP's test suite + * + * Try it with: + * + * $ php -r 'include "../PEAR/RunTest.php"; $t=new PEAR_RunTest; $o=$t->run("./pear_system.phpt");print_r($o);' + * + * + * @category pear + * @package PEAR + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.3.3 + */ +class PEAR_RunTest +{ + var $_headers = array(); + var $_logger; + var $_options; + var $_php; + var $test_count; + var $xdebug_loaded; + var $ini_overwrites = array( + 'output_handler=', + 'open_basedir=', + 'safe_mode=0', + 'disable_functions=', + 'output_buffering=Off', + 'display_errors=1', + 'log_errors=0', + 'html_errors=0', + 'track_errors=1', + 'report_memleaks=0', + 'report_zend_debug=0', + 'docref_root=', + 'docref_ext=.html', + 'error_prepend_string=', + 'error_append_string=', + 'auto_prepend_file=', + 'auto_append_file=', + 'magic_quotes_runtime=0', + 'xdebug.default_enable=0', + 'allow_url_fopen=1', + ); + + /** + * An object that supports the PEAR_Common->log() signature, or null + * @param PEAR_Common|null + */ + function PEAR_RunTest($logger = null, $options = array()) + { + $this->ini_overwrites[] = 'error_reporting=' . E_ALL; + if (is_null($logger)) { + require_once 'PEAR/Common.php'; + $logger = new PEAR_Common; + } + $this->_logger = $logger; + $this->_options = $options; + + $conf = &PEAR_Config::singleton(); + $this->_php = $conf->get('php_bin'); + } + + /** + * Taken from php-src/run-tests.php + * + * @param string $commandline command name + * @param array $env + * @param string $stdin standard input to pass to the command + * @return unknown + */ + function system_with_timeout($commandline, $env = null, $stdin = null) + { + $data = ''; + if (version_compare(phpversion(), '5.0.0', '<')) { + $proc = proc_open($commandline, array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ), $pipes); + } else { + $proc = proc_open($commandline, array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ), $pipes, null, $env, array('suppress_errors' => true)); + } + + if (!$proc) { + return false; + } + + if (is_string($stdin)) { + fwrite($pipes[0], $stdin); + } + fclose($pipes[0]); + + while (true) { + /* hide errors from interrupted syscalls */ + $r = $pipes; + $e = $w = null; + $n = @stream_select($r, $w, $e, 60); + + if ($n === 0) { + /* timed out */ + $data .= "\n ** ERROR: process timed out **\n"; + proc_terminate($proc); + return array(1234567890, $data); + } else if ($n > 0) { + $line = fread($pipes[1], 8192); + if (strlen($line) == 0) { + /* EOF */ + break; + } + $data .= $line; + } + } + if (function_exists('proc_get_status')) { + $stat = proc_get_status($proc); + if ($stat['signaled']) { + $data .= "\nTermsig=".$stat['stopsig']; + } + } + $code = proc_close($proc); + if (function_exists('proc_get_status')) { + $code = $stat['exitcode']; + } + return array($code, $data); + } + + function settings2array($settings, $ini_settings) + { + foreach ($settings as $setting) { + if (strpos($setting, '=')!== false) { + $setting = explode('=', $setting, 2); + $name = trim(strtolower($setting[0])); + $value = trim($setting[1]); + $ini_settings[$name] = $value; + } + } + return $ini_settings; + } + + function settings2params($ini_settings) + { + $settings = ''; + foreach ($ini_settings as $name => $value) { + $value = addslashes($value); + $settings .= " -d \"$name=$value\""; + } + return $settings; + } + + function runPHPUnit($file, $ini_settings = '') + { + $cmd = "$this->_php$ini_settings -f $file"; + if (isset($this->_logger)) { + $this->_logger->log(2, 'Running command "' . $cmd . '"'); + } + + $savedir = getcwd(); // in case the test moves us around + chdir(dirname($file)); + echo `$cmd`; + chdir($savedir); + return 'PASSED'; // we have no way of knowing this information so assume passing + } + + // + // Run an individual test case. + // + + function run($file, $ini_settings = '', $test_number) + { + if (empty($this->_options['cgi'])) { + // try to see if php-cgi is in the path + if (false !== $this->system_with_timeout('php-cgi -v')) { + $this->_options['cgi'] = 'php-cgi'; + } + } + if (1 < $len = strlen($this->tests_count)) { + $test_number = str_pad($test_number, $len, ' ', STR_PAD_LEFT); + $test_nr = "[$test_number/$this->tests_count] "; + } else { + $test_nr = ''; + } + + $file = realpath($file); + $section_text = $this->_readFile($file); + if (PEAR::isError($section_text)) { + return $section_text; + } + + if (isset($section_text['POST_RAW']) && isset($section_text['UPLOAD'])) { + return PEAR::raiseError("Cannot contain both POST_RAW and UPLOAD in test file: $file"); + } + + $cwd = getcwd(); + + $pass_options = ''; + if (!empty($this->_options['ini'])) { + $pass_options = $this->_options['ini']; + } + + $ini_settings = $this->settings2array($this->ini_overwrites, $ini_settings); + if ($section_text['INI']) { + if (strpos($section_text['INI'], '{PWD}') !== false) { + $section_text['INI'] = str_replace('{PWD}', dirname($file), $section_text['INI']); + } + $ini = preg_split( "/[\n\r]+/", $section_text['INI']); + $ini_settings = $this->settings2array($ini, $ini_settings); + } + $ini_settings = $this->settings2params($ini_settings); + $shortname = str_replace($cwd . DIRECTORY_SEPARATOR, '', $file); + + $tested = trim($section_text['TEST']); + $tested.= !isset($this->_options['simple']) ? "[$shortname]" : ' '; + + if (!empty($section_text['POST']) || !empty($section_text['POST_RAW']) || + !empty($section_text['UPLOAD']) || !empty($section_text['GET']) || + !empty($section_text['COOKIE']) || !empty($section_text['EXPECTHEADERS'])) { + if (empty($this->_options['cgi'])) { + if (!isset($this->_options['quiet'])) { + $this->_logger->log(0, "SKIP $test_nr$tested (reason: --cgi option needed for this test, type 'pear help run-tests')"); + } + if (isset($this->_options['tapoutput'])) { + return array('ok', ' # skip --cgi option needed for this test, "pear help run-tests" for info'); + } + return 'SKIPPED'; + } + $this->_php = $this->_options['cgi']; + } + + $temp_dir = realpath(dirname($file)); + $main_file_name = basename($file, 'phpt'); + $diff_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'diff'; + $log_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'log'; + $exp_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'exp'; + $output_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'out'; + $memcheck_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'mem'; + $temp_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'php'; + $temp_skipif = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'skip.php'; + $temp_clean = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name.'clean.php'; + $tmp_post = $temp_dir . DIRECTORY_SEPARATOR . uniqid('phpt.'); + + // unlink old test results + $this->_cleanupOldFiles($file); + + // Check if test should be skipped. + $res = $this->_runSkipIf($section_text, $temp_skipif, $tested, $ini_settings); + if (count($res) != 2) { + return $res; + } + $info = $res['info']; + $warn = $res['warn']; + + // We've satisfied the preconditions - run the test! + if (isset($this->_options['coverage']) && $this->xdebug_loaded) { + $len_f = 5; + if (substr($section_text['FILE'], 0, 5) != '')); + $xdebug_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'xdebug'; + $text.= "\n" . + "\n" . '$xdebug = var_export(xdebug_get_code_coverage(), true);' . + "\n" . 'file_put_contents(\'' . $xdebug_file . '\', $xdebug);' . + "\n" . 'xdebug_stop_code_coverage();' . "\n" . '?>'; + + $this->save_text($temp_file, $text); + } else { + $this->save_text($temp_file, $section_text['FILE']); + } + + $args = $section_text['ARGS'] ? ' -- '.$section_text['ARGS'] : ''; + $cmd = "$this->_php$ini_settings \"$temp_file\" $args 2>&1"; + if (isset($this->_logger)) { + $this->_logger->log(2, 'Running command "' . $cmd . '"'); + } + + // Reset environment from any previous test. + $env = $this->_resetEnv($section_text, $temp_file); + + $section_text = $this->_processUpload($section_text, $file); + if (PEAR::isError($section_text)) { + return $section_text; + } + + if (array_key_exists('POST_RAW', $section_text) && !empty($section_text['POST_RAW'])) { + $post = trim($section_text['POST_RAW']); + $raw_lines = explode("\n", $post); + + $request = ''; + $started = false; + foreach ($raw_lines as $i => $line) { + if (empty($env['CONTENT_TYPE']) && + preg_match('/^Content-Type:(.*)/i', $line, $res)) { + $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1])); + continue; + } + if ($started) { + $request .= "\n"; + } + $started = true; + $request .= $line; + } + + $env['CONTENT_LENGTH'] = strlen($request); + $env['REQUEST_METHOD'] = 'POST'; + + $this->save_text($tmp_post, $request); + $cmd = "$this->_php$pass_options$ini_settings \"$temp_file\" 2>&1 < $tmp_post"; + } elseif (array_key_exists('POST', $section_text) && !empty($section_text['POST'])) { + $post = trim($section_text['POST']); + $this->save_text($tmp_post, $post); + $content_length = strlen($post); + + $env['REQUEST_METHOD'] = 'POST'; + $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + $env['CONTENT_LENGTH'] = $content_length; + + $cmd = "$this->_php$pass_options$ini_settings \"$temp_file\" 2>&1 < $tmp_post"; + } else { + $env['REQUEST_METHOD'] = 'GET'; + $env['CONTENT_TYPE'] = ''; + $env['CONTENT_LENGTH'] = ''; + } + + if (OS_WINDOWS && isset($section_text['RETURNS'])) { + ob_start(); + system($cmd, $return_value); + $out = ob_get_contents(); + ob_end_clean(); + $section_text['RETURNS'] = (int) trim($section_text['RETURNS']); + $returnfail = ($return_value != $section_text['RETURNS']); + } else { + $returnfail = false; + $stdin = isset($section_text['STDIN']) ? $section_text['STDIN'] : null; + $out = $this->system_with_timeout($cmd, $env, $stdin); + $return_value = $out[0]; + $out = $out[1]; + } + + $output = preg_replace('/\r\n/', "\n", trim($out)); + + if (isset($tmp_post) && realpath($tmp_post) && file_exists($tmp_post)) { + @unlink(realpath($tmp_post)); + } + chdir($cwd); // in case the test moves us around + + $this->_testCleanup($section_text, $temp_clean); + + /* when using CGI, strip the headers from the output */ + $output = $this->_stripHeadersCGI($output); + + if (isset($section_text['EXPECTHEADERS'])) { + $testheaders = $this->_processHeaders($section_text['EXPECTHEADERS']); + $missing = array_diff_assoc($testheaders, $this->_headers); + $changed = ''; + foreach ($missing as $header => $value) { + if (isset($this->_headers[$header])) { + $changed .= "-$header: $value\n+$header: "; + $changed .= $this->_headers[$header]; + } else { + $changed .= "-$header: $value\n"; + } + } + if ($missing) { + // tack on failed headers to output: + $output .= "\n====EXPECTHEADERS FAILURE====:\n$changed"; + } + } + // Does the output match what is expected? + do { + if (isset($section_text['EXPECTF']) || isset($section_text['EXPECTREGEX'])) { + if (isset($section_text['EXPECTF'])) { + $wanted = trim($section_text['EXPECTF']); + } else { + $wanted = trim($section_text['EXPECTREGEX']); + } + $wanted_re = preg_replace('/\r\n/', "\n", $wanted); + if (isset($section_text['EXPECTF'])) { + $wanted_re = preg_quote($wanted_re, '/'); + // Stick to basics + $wanted_re = str_replace("%s", ".+?", $wanted_re); //not greedy + $wanted_re = str_replace("%i", "[+\-]?[0-9]+", $wanted_re); + $wanted_re = str_replace("%d", "[0-9]+", $wanted_re); + $wanted_re = str_replace("%x", "[0-9a-fA-F]+", $wanted_re); + $wanted_re = str_replace("%f", "[+\-]?\.?[0-9]+\.?[0-9]*(E-?[0-9]+)?", $wanted_re); + $wanted_re = str_replace("%c", ".", $wanted_re); + // %f allows two points "-.0.0" but that is the best *simple* expression + } + /* DEBUG YOUR REGEX HERE + var_dump($wanted_re); + print(str_repeat('=', 80) . "\n"); + var_dump($output); + */ + if (!$returnfail && preg_match("/^$wanted_re\$/s", $output)) { + if (file_exists($temp_file)) { + unlink($temp_file); + } + if (array_key_exists('FAIL', $section_text)) { + break; + } + if (!isset($this->_options['quiet'])) { + $this->_logger->log(0, "PASS $test_nr$tested$info"); + } + if (isset($this->_options['tapoutput'])) { + return array('ok', ' - ' . $tested); + } + return 'PASSED'; + } + } else { + if (isset($section_text['EXPECTFILE'])) { + $f = $temp_dir . '/' . trim($section_text['EXPECTFILE']); + if (!($fp = @fopen($f, 'rb'))) { + return PEAR::raiseError('--EXPECTFILE-- section file ' . + $f . ' not found'); + } + fclose($fp); + $section_text['EXPECT'] = file_get_contents($f); + } + $wanted = preg_replace('/\r\n/', "\n", trim($section_text['EXPECT'])); + // compare and leave on success + if (!$returnfail && 0 == strcmp($output, $wanted)) { + if (file_exists($temp_file)) { + unlink($temp_file); + } + if (array_key_exists('FAIL', $section_text)) { + break; + } + if (!isset($this->_options['quiet'])) { + $this->_logger->log(0, "PASS $test_nr$tested$info"); + } + if (isset($this->_options['tapoutput'])) { + return array('ok', ' - ' . $tested); + } + return 'PASSED'; + } + } + } while (false); + + if (array_key_exists('FAIL', $section_text)) { + // we expect a particular failure + // this is only used for testing PEAR_RunTest + $expectf = isset($section_text['EXPECTF']) ? $wanted_re : null; + $faildiff = $this->generate_diff($wanted, $output, null, $expectf); + $faildiff = preg_replace('/\r/', '', $faildiff); + $wanted = preg_replace('/\r/', '', trim($section_text['FAIL'])); + if ($faildiff == $wanted) { + if (!isset($this->_options['quiet'])) { + $this->_logger->log(0, "PASS $test_nr$tested$info"); + } + if (isset($this->_options['tapoutput'])) { + return array('ok', ' - ' . $tested); + } + return 'PASSED'; + } + unset($section_text['EXPECTF']); + $output = $faildiff; + if (isset($section_text['RETURNS'])) { + return PEAR::raiseError('Cannot have both RETURNS and FAIL in the same test: ' . + $file); + } + } + + // Test failed so we need to report details. + $txt = $warn ? 'WARN ' : 'FAIL '; + $this->_logger->log(0, $txt . $test_nr . $tested . $info); + + // write .exp + $res = $this->_writeLog($exp_filename, $wanted); + if (PEAR::isError($res)) { + return $res; + } + + // write .out + $res = $this->_writeLog($output_filename, $output); + if (PEAR::isError($res)) { + return $res; + } + + // write .diff + $returns = isset($section_text['RETURNS']) ? + array(trim($section_text['RETURNS']), $return_value) : null; + $expectf = isset($section_text['EXPECTF']) ? $wanted_re : null; + $data = $this->generate_diff($wanted, $output, $returns, $expectf); + $res = $this->_writeLog($diff_filename, $data); + if (PEAR::isError($res)) { + return $res; + } + + // write .log + $data = " +---- EXPECTED OUTPUT +$wanted +---- ACTUAL OUTPUT +$output +---- FAILED +"; + + if ($returnfail) { + $data .= " +---- EXPECTED RETURN +$section_text[RETURNS] +---- ACTUAL RETURN +$return_value +"; + } + + $res = $this->_writeLog($log_filename, $data); + if (PEAR::isError($res)) { + return $res; + } + + if (isset($this->_options['tapoutput'])) { + $wanted = explode("\n", $wanted); + $wanted = "# Expected output:\n#\n#" . implode("\n#", $wanted); + $output = explode("\n", $output); + $output = "#\n#\n# Actual output:\n#\n#" . implode("\n#", $output); + return array($wanted . $output . 'not ok', ' - ' . $tested); + } + return $warn ? 'WARNED' : 'FAILED'; + } + + function generate_diff($wanted, $output, $rvalue, $wanted_re) + { + $w = explode("\n", $wanted); + $o = explode("\n", $output); + $wr = explode("\n", $wanted_re); + $w1 = array_diff_assoc($w, $o); + $o1 = array_diff_assoc($o, $w); + $o2 = $w2 = array(); + foreach ($w1 as $idx => $val) { + if (!$wanted_re || !isset($wr[$idx]) || !isset($o1[$idx]) || + !preg_match('/^' . $wr[$idx] . '\\z/', $o1[$idx])) { + $w2[sprintf("%03d<", $idx)] = sprintf("%03d- ", $idx + 1) . $val; + } + } + foreach ($o1 as $idx => $val) { + if (!$wanted_re || !isset($wr[$idx]) || + !preg_match('/^' . $wr[$idx] . '\\z/', $val)) { + $o2[sprintf("%03d>", $idx)] = sprintf("%03d+ ", $idx + 1) . $val; + } + } + $diff = array_merge($w2, $o2); + ksort($diff); + $extra = $rvalue ? "##EXPECTED: $rvalue[0]\r\n##RETURNED: $rvalue[1]" : ''; + return implode("\r\n", $diff) . $extra; + } + + // Write the given text to a temporary file, and return the filename. + function save_text($filename, $text) + { + if (!$fp = fopen($filename, 'w')) { + return PEAR::raiseError("Cannot open file '" . $filename . "' (save_text)"); + } + fwrite($fp, $text); + fclose($fp); + if (1 < DETAILED) echo " +FILE $filename {{{ +$text +}}} +"; + } + + function _cleanupOldFiles($file) + { + $temp_dir = realpath(dirname($file)); + $mainFileName = basename($file, 'phpt'); + $diff_filename = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'diff'; + $log_filename = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'log'; + $exp_filename = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'exp'; + $output_filename = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'out'; + $memcheck_filename = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'mem'; + $temp_file = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'php'; + $temp_skipif = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'skip.php'; + $temp_clean = $temp_dir . DIRECTORY_SEPARATOR . $mainFileName.'clean.php'; + $tmp_post = $temp_dir . DIRECTORY_SEPARATOR . uniqid('phpt.'); + + // unlink old test results + @unlink($diff_filename); + @unlink($log_filename); + @unlink($exp_filename); + @unlink($output_filename); + @unlink($memcheck_filename); + @unlink($temp_file); + @unlink($temp_skipif); + @unlink($tmp_post); + @unlink($temp_clean); + } + + function _runSkipIf($section_text, $temp_skipif, $tested, $ini_settings) + { + $info = ''; + $warn = false; + if (array_key_exists('SKIPIF', $section_text) && trim($section_text['SKIPIF'])) { + $this->save_text($temp_skipif, $section_text['SKIPIF']); + $output = $this->system_with_timeout("$this->_php$ini_settings -f \"$temp_skipif\""); + $output = $output[1]; + $loutput = ltrim($output); + unlink($temp_skipif); + if (!strncasecmp('skip', $loutput, 4)) { + $skipreason = "SKIP $tested"; + if (preg_match('/^\s*skip\s*(.+)\s*/i', $output, $m)) { + $skipreason .= '(reason: ' . $m[1] . ')'; + } + if (!isset($this->_options['quiet'])) { + $this->_logger->log(0, $skipreason); + } + if (isset($this->_options['tapoutput'])) { + return array('ok', ' # skip ' . $reason); + } + return 'SKIPPED'; + } + + if (!strncasecmp('info', $loutput, 4) + && preg_match('/^\s*info\s*(.+)\s*/i', $output, $m)) { + $info = " (info: $m[1])"; + } + + if (!strncasecmp('warn', $loutput, 4) + && preg_match('/^\s*warn\s*(.+)\s*/i', $output, $m)) { + $warn = true; /* only if there is a reason */ + $info = " (warn: $m[1])"; + } + } + + return array('warn' => $warn, 'info' => $info); + } + + function _stripHeadersCGI($output) + { + $this->headers = array(); + if (!empty($this->_options['cgi']) && + $this->_php == $this->_options['cgi'] && + preg_match("/^(.*?)(?:\n\n(.*)|\\z)/s", $output, $match)) { + $output = isset($match[2]) ? trim($match[2]) : ''; + $this->_headers = $this->_processHeaders($match[1]); + } + + return $output; + } + + /** + * Return an array that can be used with array_diff() to compare headers + * + * @param string $text + */ + function _processHeaders($text) + { + $headers = array(); + $rh = preg_split("/[\n\r]+/", $text); + foreach ($rh as $line) { + if (strpos($line, ':')!== false) { + $line = explode(':', $line, 2); + $headers[trim($line[0])] = trim($line[1]); + } + } + return $headers; + } + + function _readFile($file) + { + // Load the sections of the test file. + $section_text = array( + 'TEST' => '(unnamed test)', + 'SKIPIF' => '', + 'GET' => '', + 'COOKIE' => '', + 'POST' => '', + 'ARGS' => '', + 'INI' => '', + 'CLEAN' => '', + ); + + if (!is_file($file) || !$fp = fopen($file, "r")) { + return PEAR::raiseError("Cannot open test file: $file"); + } + + $section = ''; + while (!feof($fp)) { + $line = fgets($fp); + + // Match the beginning of a section. + if (preg_match('/^--([_A-Z]+)--/', $line, $r)) { + $section = $r[1]; + $section_text[$section] = ''; + continue; + } elseif (empty($section)) { + fclose($fp); + return PEAR::raiseError("Invalid sections formats in test file: $file"); + } + + // Add to the section text. + $section_text[$section] .= $line; + } + fclose($fp); + + return $section_text; + } + + function _writeLog($logname, $data) + { + if (!$log = fopen($logname, 'w')) { + return PEAR::raiseError("Cannot create test log - $logname"); + } + fwrite($log, $data); + fclose($log); + } + + function _resetEnv($section_text, $temp_file) + { + $env = $_ENV; + $env['REDIRECT_STATUS'] = ''; + $env['QUERY_STRING'] = ''; + $env['PATH_TRANSLATED'] = ''; + $env['SCRIPT_FILENAME'] = ''; + $env['REQUEST_METHOD'] = ''; + $env['CONTENT_TYPE'] = ''; + $env['CONTENT_LENGTH'] = ''; + if (!empty($section_text['ENV'])) { + foreach (explode("\n", trim($section_text['ENV'])) as $e) { + $e = explode('=', trim($e), 2); + if (!empty($e[0]) && isset($e[1])) { + $env[$e[0]] = $e[1]; + } + } + } + if (array_key_exists('GET', $section_text)) { + $env['QUERY_STRING'] = trim($section_text['GET']); + } else { + $env['QUERY_STRING'] = ''; + } + if (array_key_exists('COOKIE', $section_text)) { + $env['HTTP_COOKIE'] = trim($section_text['COOKIE']); + } else { + $env['HTTP_COOKIE'] = ''; + } + $env['REDIRECT_STATUS'] = '1'; + $env['PATH_TRANSLATED'] = $temp_file; + $env['SCRIPT_FILENAME'] = $temp_file; + + return $env; + } + + function _processUpload($section_text, $file) + { + if (array_key_exists('UPLOAD', $section_text) && !empty($section_text['UPLOAD'])) { + $upload_files = trim($section_text['UPLOAD']); + $upload_files = explode("\n", $upload_files); + + $request = "Content-Type: multipart/form-data; boundary=---------------------------20896060251896012921717172737\n" . + "-----------------------------20896060251896012921717172737\n"; + foreach ($upload_files as $fileinfo) { + $fileinfo = explode('=', $fileinfo); + if (count($fileinfo) != 2) { + return PEAR::raiseError("Invalid UPLOAD section in test file: $file"); + } + if (!realpath(dirname($file) . '/' . $fileinfo[1])) { + return PEAR::raiseError("File for upload does not exist: $fileinfo[1] " . + "in test file: $file"); + } + $file_contents = file_get_contents(dirname($file) . '/' . $fileinfo[1]); + $fileinfo[1] = basename($fileinfo[1]); + $request .= "Content-Disposition: form-data; name=\"$fileinfo[0]\"; filename=\"$fileinfo[1]\"\n"; + $request .= "Content-Type: text/plain\n\n"; + $request .= $file_contents . "\n" . + "-----------------------------20896060251896012921717172737\n"; + } + + if (array_key_exists('POST', $section_text) && !empty($section_text['POST'])) { + // encode POST raw + $post = trim($section_text['POST']); + $post = explode('&', $post); + foreach ($post as $i => $post_info) { + $post_info = explode('=', $post_info); + if (count($post_info) != 2) { + return PEAR::raiseError("Invalid POST data in test file: $file"); + } + $post_info[0] = rawurldecode($post_info[0]); + $post_info[1] = rawurldecode($post_info[1]); + $post[$i] = $post_info; + } + foreach ($post as $post_info) { + $request .= "Content-Disposition: form-data; name=\"$post_info[0]\"\n\n"; + $request .= $post_info[1] . "\n" . + "-----------------------------20896060251896012921717172737\n"; + } + unset($section_text['POST']); + } + $section_text['POST_RAW'] = $request; + } + + return $section_text; + } + + function _testCleanup($section_text, $temp_clean) + { + if ($section_text['CLEAN']) { + // perform test cleanup + $this->save_text($temp_clean, $section_text['CLEAN']); + $this->system_with_timeout("$this->_php $temp_clean"); + if (file_exists($temp_clean)) { + unlink($temp_clean); + } + } + } +} diff --git a/PEAR/Task/Common.php b/PEAR/Task/Common.php new file mode 100644 index 0000000..5532549 --- /dev/null +++ b/PEAR/Task/Common.php @@ -0,0 +1,208 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Common.php,v 1.16 2006/11/12 05:02:41 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/**#@+ + * Error codes for task validation routines + */ +define('PEAR_TASK_ERROR_NOATTRIBS', 1); +define('PEAR_TASK_ERROR_MISSING_ATTRIB', 2); +define('PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE', 3); +define('PEAR_TASK_ERROR_INVALID', 4); +/**#@-*/ +define('PEAR_TASK_PACKAGE', 1); +define('PEAR_TASK_INSTALL', 2); +define('PEAR_TASK_PACKAGEANDINSTALL', 3); +/** + * A task is an operation that manipulates the contents of a file. + * + * Simple tasks operate on 1 file. Multiple tasks are executed after all files have been + * processed and installed, and are designed to operate on all files containing the task. + * The Post-install script task simply takes advantage of the fact that it will be run + * after installation, replace is a simple task. + * + * Combining tasks is possible, but ordering is significant. + * + * + * + * + * + * + * This will first replace any instance of @data-dir@ in the test.php file + * with the path to the current data directory. Then, it will include the + * test.php file and run the script it contains to configure the package post-installation. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + * @abstract + */ +class PEAR_Task_Common +{ + /** + * Valid types for this version are 'simple' and 'multiple' + * + * - simple tasks operate on the contents of a file and write out changes to disk + * - multiple tasks operate on the contents of many files and write out the + * changes directly to disk + * + * Child task classes must override this property. + * @access protected + */ + var $type = 'simple'; + /** + * Determines which install phase this task is executed under + */ + var $phase = PEAR_TASK_INSTALL; + /** + * @access protected + */ + var $config; + /** + * @access protected + */ + var $registry; + /** + * @access protected + */ + var $logger; + /** + * @access protected + */ + var $installphase; + /** + * @param PEAR_Config + * @param PEAR_Common + */ + function PEAR_Task_Common(&$config, &$logger, $phase) + { + $this->config = &$config; + $this->registry = &$config->getRegistry(); + $this->logger = &$logger; + $this->installphase = $phase; + if ($this->type == 'multiple') { + $GLOBALS['_PEAR_TASK_POSTINSTANCES'][get_class($this)][] = &$this; + } + } + + /** + * Validate the basic contents of a task tag. + * @param PEAR_PackageFile_v2 + * @param array + * @param PEAR_Config + * @param array the entire parsed tag + * @return true|array On error, return an array in format: + * array(PEAR_TASK_ERROR_???[, param1][, param2][, ...]) + * + * For PEAR_TASK_ERROR_MISSING_ATTRIB, pass the attribute name in + * For PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, pass the attribute name and an array + * of legal values in + * @static + * @abstract + */ + function validateXml($pkg, $xml, &$config, $fileXml) + { + } + + /** + * Initialize a task instance with the parameters + * @param array raw, parsed xml + * @param array attributes from the tag containing this task + * @param string|null last installed version of this package + * @abstract + */ + function init($xml, $fileAttributes, $lastVersion) + { + } + + /** + * Begin a task processing session. All multiple tasks will be processed after each file + * has been successfully installed, all simple tasks should perform their task here and + * return any errors using the custom throwError() method to allow forward compatibility + * + * This method MUST NOT write out any changes to disk + * @param PEAR_PackageFile_v2 + * @param string file contents + * @param string the eventual final file location (informational only) + * @return string|false|PEAR_Error false to skip this file, PEAR_Error to fail + * (use $this->throwError), otherwise return the new contents + * @abstract + */ + function startSession($pkg, $contents, $dest) + { + } + + /** + * This method is used to process each of the tasks for a particular multiple class + * type. Simple tasks need not implement this method. + * @param array an array of tasks + * @access protected + * @static + * @abstract + */ + function run($tasks) + { + } + + /** + * @static + * @final + */ + function hasPostinstallTasks() + { + return isset($GLOBALS['_PEAR_TASK_POSTINSTANCES']); + } + + /** + * @static + * @final + */ + function runPostinstallTasks() + { + foreach ($GLOBALS['_PEAR_TASK_POSTINSTANCES'] as $class => $tasks) { + $err = call_user_func(array($class, 'run'), + $GLOBALS['_PEAR_TASK_POSTINSTANCES'][$class]); + if ($err) { + return PEAR_Task_Common::throwError($err); + } + } + unset($GLOBALS['_PEAR_TASK_POSTINSTANCES']); + } + + /** + * Determines whether a role is a script + * @return bool + */ + function isScript() + { + return $this->type == 'script'; + } + + function throwError($msg, $code = -1) + { + include_once 'PEAR.php'; + return PEAR::raiseError($msg, $code); + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Postinstallscript.php b/PEAR/Task/Postinstallscript.php new file mode 100644 index 0000000..7b485da --- /dev/null +++ b/PEAR/Task/Postinstallscript.php @@ -0,0 +1,329 @@ + + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Postinstallscript.php,v 1.18 2006/02/08 01:21:47 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Common.php'; +/** + * Implements the postinstallscript file task. + * + * Note that post-install scripts are handled separately from installation, by the + * "pear run-scripts" command + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Task_Postinstallscript extends PEAR_Task_Common +{ + var $type = 'script'; + var $_class; + var $_params; + var $_obj; + /** + * + * @var PEAR_PackageFile_v2 + */ + var $_pkg; + var $_contents; + var $phase = PEAR_TASK_INSTALL; + + /** + * Validate the raw xml at parsing-time. + * + * This also attempts to validate the script to make sure it meets the criteria + * for a post-install script + * @param PEAR_PackageFile_v2 + * @param array The XML contents of the tag + * @param PEAR_Config + * @param array the entire parsed tag + * @static + */ + function validateXml($pkg, $xml, &$config, $fileXml) + { + if ($fileXml['role'] != 'php') { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" must be role="php"'); + } + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $file = $pkg->getFileContents($fileXml['name']); + if (PEAR::isError($file)) { + PEAR::popErrorHandling(); + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" is not valid: ' . + $file->getMessage()); + } elseif ($file === null) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" could not be retrieved for processing!'); + } else { + $analysis = $pkg->analyzeSourceCode($file, true); + if (!$analysis) { + PEAR::popErrorHandling(); + $warnings = ''; + foreach ($pkg->getValidationWarnings() as $warn) { + $warnings .= $warn['message'] . "\n"; + } + return array(PEAR_TASK_ERROR_INVALID, 'Analysis of post-install script "' . + $fileXml['name'] . '" failed: ' . $warnings); + } + if (count($analysis['declared_classes']) != 1) { + PEAR::popErrorHandling(); + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" must declare exactly 1 class'); + } + $class = $analysis['declared_classes'][0]; + if ($class != str_replace(array('/', '.php'), array('_', ''), + $fileXml['name']) . '_postinstall') { + PEAR::popErrorHandling(); + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" class "' . $class . '" must be named "' . + str_replace(array('/', '.php'), array('_', ''), + $fileXml['name']) . '_postinstall"'); + } + if (!isset($analysis['declared_methods'][$class])) { + PEAR::popErrorHandling(); + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" must declare methods init() and run()'); + } + $methods = array('init' => 0, 'run' => 1); + foreach ($analysis['declared_methods'][$class] as $method) { + if (isset($methods[$method])) { + unset($methods[$method]); + } + } + if (count($methods)) { + PEAR::popErrorHandling(); + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" must declare methods init() and run()'); + } + } + PEAR::popErrorHandling(); + $definedparams = array(); + $tasksNamespace = $pkg->getTasksNs() . ':'; + if (!isset($xml[$tasksNamespace . 'paramgroup']) && isset($xml['paramgroup'])) { + // in order to support the older betas, which did not expect internal tags + // to also use the namespace + $tasksNamespace = ''; + } + if (isset($xml[$tasksNamespace . 'paramgroup'])) { + $params = $xml[$tasksNamespace . 'paramgroup']; + if (!is_array($params) || !isset($params[0])) { + $params = array($params); + } + foreach ($params as $param) { + if (!isset($param[$tasksNamespace . 'id'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" must have ' . + 'an ' . $tasksNamespace . 'id> tag'); + } + if (isset($param[$tasksNamespace . 'name'])) { + if (!in_array($param[$tasksNamespace . 'name'], $definedparams)) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" ' . $tasksNamespace . + 'paramgroup> id "' . $param[$tasksNamespace . 'id'] . + '" parameter "' . $param[$tasksNamespace . 'name'] . + '" has not been previously defined'); + } + if (!isset($param[$tasksNamespace . 'conditiontype'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" ' . $tasksNamespace . + 'paramgroup> id "' . $param[$tasksNamespace . 'id'] . + '" must have a ' . $tasksNamespace . + 'conditiontype> tag containing either "=", ' . + '"!=", or "preg_match"'); + } + if (!in_array($param[$tasksNamespace . 'conditiontype'], + array('=', '!=', 'preg_match'))) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" ' . $tasksNamespace . + 'paramgroup> id "' . $param[$tasksNamespace . 'id'] . + '" must have a ' . $tasksNamespace . + 'conditiontype> tag containing either "=", ' . + '"!=", or "preg_match"'); + } + if (!isset($param[$tasksNamespace . 'value'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" ' . $tasksNamespace . + 'paramgroup> id "' . $param[$tasksNamespace . 'id'] . + '" must have a ' . $tasksNamespace . + 'value> tag containing expected parameter value'); + } + } + if (isset($param[$tasksNamespace . 'instructions'])) { + if (!is_string($param[$tasksNamespace . 'instructions'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" ' . $tasksNamespace . + 'paramgroup> id "' . $param[$tasksNamespace . 'id'] . + '" ' . $tasksNamespace . 'instructions> must be simple text'); + } + } + if (!isset($param[$tasksNamespace . 'param'])) { + continue; // is no longer required + } + $subparams = $param[$tasksNamespace . 'param']; + if (!is_array($subparams) || !isset($subparams[0])) { + $subparams = array($subparams); + } + foreach ($subparams as $subparam) { + if (!isset($subparam[$tasksNamespace . 'name'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" parameter for ' . + $tasksNamespace . 'paramgroup> id "' . + $param[$tasksNamespace . 'id'] . '" must have ' . + 'a ' . $tasksNamespace . 'name> tag'); + } + if (!preg_match('/[a-zA-Z0-9]+/', + $subparam[$tasksNamespace . 'name'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" parameter "' . + $subparam[$tasksNamespace . 'name'] . + '" for ' . $tasksNamespace . 'paramgroup> id "' . + $param[$tasksNamespace . 'id'] . + '" is not a valid name. Must contain only alphanumeric characters'); + } + if (!isset($subparam[$tasksNamespace . 'prompt'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" parameter "' . + $subparam[$tasksNamespace . 'name'] . + '" for ' . $tasksNamespace . 'paramgroup> id "' . + $param[$tasksNamespace . 'id'] . + '" must have a ' . $tasksNamespace . 'prompt> tag'); + } + if (!isset($subparam[$tasksNamespace . 'type'])) { + return array(PEAR_TASK_ERROR_INVALID, 'Post-install script "' . + $fileXml['name'] . '" parameter "' . + $subparam[$tasksNamespace . 'name'] . + '" for ' . $tasksNamespace . 'paramgroup> id "' . + $param[$tasksNamespace . 'id'] . + '" must have a ' . $tasksNamespace . 'type> tag'); + } + $definedparams[] = $param[$tasksNamespace . 'id'] . '::' . + $subparam[$tasksNamespace . 'name']; + } + } + } + return true; + } + + /** + * Initialize a task instance with the parameters + * @param array raw, parsed xml + * @param array attributes from the tag containing this task + * @param string|null last installed version of this package, if any (useful for upgrades) + */ + function init($xml, $fileattribs, $lastversion) + { + $this->_class = str_replace('/', '_', $fileattribs['name']); + $this->_filename = $fileattribs['name']; + $this->_class = str_replace ('.php', '', $this->_class) . '_postinstall'; + $this->_params = $xml; + $this->_lastversion = $lastversion; + } + + /** + * Strip the tasks: namespace from internal params + * + * @access private + */ + function _stripNamespace($params = null) + { + if ($params === null) { + $params = array(); + if (!is_array($this->_params)) { + return; + } + foreach ($this->_params as $i => $param) { + if (is_array($param)) { + $param = $this->_stripNamespace($param); + } + $params[str_replace($this->_pkg->getTasksNs() . ':', '', $i)] = $param; + } + $this->_params = $params; + } else { + $newparams = array(); + foreach ($params as $i => $param) { + if (is_array($param)) { + $param = $this->_stripNamespace($param); + } + $newparams[str_replace($this->_pkg->getTasksNs() . ':', '', $i)] = $param; + } + return $newparams; + } + } + + /** + * Unlike other tasks, the installed file name is passed in instead of the file contents, + * because this task is handled post-installation + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param string file name + * @return bool|PEAR_Error false to skip this file, PEAR_Error to fail + * (use $this->throwError) + */ + function startSession($pkg, $contents) + { + if ($this->installphase != PEAR_TASK_INSTALL) { + return false; + } + // remove the tasks: namespace if present + $this->_pkg = $pkg; + $this->_stripNamespace(); + $this->logger->log(0, 'Including external post-installation script "' . + $contents . '" - any errors are in this script'); + include_once $contents; + if (class_exists($this->_class)) { + $this->logger->log(0, 'Inclusion succeeded'); + } else { + return $this->throwError('init of post-install script class "' . $this->_class + . '" failed'); + } + $this->_obj = new $this->_class; + $this->logger->log(1, 'running post-install script "' . $this->_class . '->init()"'); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $res = $this->_obj->init($this->config, $pkg, $this->_lastversion); + PEAR::popErrorHandling(); + if ($res) { + $this->logger->log(0, 'init succeeded'); + } else { + return $this->throwError('init of post-install script "' . $this->_class . + '->init()" failed'); + } + $this->_contents = $contents; + return true; + } + + /** + * No longer used + * @see PEAR_PackageFile_v2::runPostinstallScripts() + * @param array an array of tasks + * @param string install or upgrade + * @access protected + * @static + */ + function run() + { + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Postinstallscript/rw.php b/PEAR/Task/Postinstallscript/rw.php new file mode 100644 index 0000000..ffcc5b9 --- /dev/null +++ b/PEAR/Task/Postinstallscript/rw.php @@ -0,0 +1,176 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: rw.php,v 1.11 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a10 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Postinstallscript.php'; +/** + * Abstracts the postinstallscript file task xml. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a10 + */ +class PEAR_Task_Postinstallscript_rw extends PEAR_Task_Postinstallscript +{ + /** + * parent package file object + * + * @var PEAR_PackageFile_v2_rw + */ + var $_pkg; + /** + * Enter description here... + * + * @param PEAR_PackageFile_v2_rw $pkg + * @param PEAR_Config $config + * @param PEAR_Frontend $logger + * @param array $fileXml + * @return PEAR_Task_Postinstallscript_rw + */ + function PEAR_Task_Postinstallscript_rw(&$pkg, &$config, &$logger, $fileXml) + { + parent::PEAR_Task_Common($config, $logger, PEAR_TASK_PACKAGE); + $this->_contents = $fileXml; + $this->_pkg = &$pkg; + $this->_params = array(); + } + + function validate() + { + return $this->validateXml($this->_pkg, $this->_params, $this->config, $this->_contents); + } + + function getName() + { + return 'postinstallscript'; + } + + /** + * add a simple to the post-install script + * + * Order is significant, so call this method in the same + * sequence the users should see the paramgroups. The $params + * parameter should either be the result of a call to {@link getParam()} + * or an array of calls to getParam(). + * + * Use {@link addConditionTypeGroup()} to add a containing + * a tag + * @param string $id id as seen by the script + * @param array|false $params array of getParam() calls, or false for no params + * @param string|false $instructions + */ + function addParamGroup($id, $params = false, $instructions = false) + { + if ($params && isset($params[0]) && !isset($params[1])) { + $params = $params[0]; + } + $stuff = + array( + $this->_pkg->getTasksNs() . ':id' => $id, + ); + if ($instructions) { + $stuff[$this->_pkg->getTasksNs() . ':instructions'] = $instructions; + } + if ($params) { + $stuff[$this->_pkg->getTasksNs() . ':param'] = $params; + } + $this->_params[$this->_pkg->getTasksNs() . ':paramgroup'][] = $stuff; + } + + /** + * add a complex to the post-install script with conditions + * + * This inserts a with + * + * Order is significant, so call this method in the same + * sequence the users should see the paramgroups. The $params + * parameter should either be the result of a call to {@link getParam()} + * or an array of calls to getParam(). + * + * Use {@link addParamGroup()} to add a simple + * + * @param string $id id as seen by the script + * @param string $oldgroup id of the section referenced by + * + * @param string $param name of the from the older section referenced + * by + * @param string $value value to match of the parameter + * @param string $conditiontype one of '=', '!=', 'preg_match' + * @param array|false $params array of getParam() calls, or false for no params + * @param string|false $instructions + */ + function addConditionTypeGroup($id, $oldgroup, $param, $value, $conditiontype = '=', + $params = false, $instructions = false) + { + if ($params && isset($params[0]) && !isset($params[1])) { + $params = $params[0]; + } + $stuff = + array( + $this->_pkg->getTasksNs() . ':id' => $id, + ); + if ($instructions) { + $stuff[$this->_pkg->getTasksNs() . ':instructions'] = $instructions; + } + $stuff[$this->_pkg->getTasksNs() . ':name'] = $oldgroup . '::' . $param; + $stuff[$this->_pkg->getTasksNs() . ':conditiontype'] = $conditiontype; + $stuff[$this->_pkg->getTasksNs() . ':value'] = $value; + if ($params) { + $stuff[$this->_pkg->getTasksNs() . ':param'] = $params; + } + $this->_params[$this->_pkg->getTasksNs() . ':paramgroup'][] = $stuff; + } + + function getXml() + { + return $this->_params; + } + + /** + * Use to set up a param tag for use in creating a paramgroup + * @static + */ + function getParam($name, $prompt, $type = 'string', $default = null) + { + if ($default !== null) { + return + array( + $this->_pkg->getTasksNs() . ':name' => $name, + $this->_pkg->getTasksNs() . ':prompt' => $prompt, + $this->_pkg->getTasksNs() . ':type' => $type, + $this->_pkg->getTasksNs() . ':default' => $default + ); + } + return + array( + $this->_pkg->getTasksNs() . ':name' => $name, + $this->_pkg->getTasksNs() . ':prompt' => $prompt, + $this->_pkg->getTasksNs() . ':type' => $type, + ); + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Replace.php b/PEAR/Task/Replace.php new file mode 100644 index 0000000..37a91b2 --- /dev/null +++ b/PEAR/Task/Replace.php @@ -0,0 +1,182 @@ + + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Replace.php,v 1.15 2006/03/02 18:14:13 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Common.php'; +/** + * Implements the replace file task. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Task_Replace extends PEAR_Task_Common +{ + var $type = 'simple'; + var $phase = PEAR_TASK_PACKAGEANDINSTALL; + var $_replacements; + + /** + * Validate the raw xml at parsing-time. + * @param PEAR_PackageFile_v2 + * @param array raw, parsed xml + * @param PEAR_Config + * @static + */ + function validateXml($pkg, $xml, &$config, $fileXml) + { + if (!isset($xml['attribs'])) { + return array(PEAR_TASK_ERROR_NOATTRIBS); + } + if (!isset($xml['attribs']['type'])) { + return array(PEAR_TASK_ERROR_MISSING_ATTRIB, 'type'); + } + if (!isset($xml['attribs']['to'])) { + return array(PEAR_TASK_ERROR_MISSING_ATTRIB, 'to'); + } + if (!isset($xml['attribs']['from'])) { + return array(PEAR_TASK_ERROR_MISSING_ATTRIB, 'from'); + } + if ($xml['attribs']['type'] == 'pear-config') { + if (!in_array($xml['attribs']['to'], $config->getKeys())) { + return array(PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'to', $xml['attribs']['to'], + $config->getKeys()); + } + } elseif ($xml['attribs']['type'] == 'php-const') { + if (defined($xml['attribs']['to'])) { + return true; + } else { + return array(PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'to', $xml['attribs']['to'], + array('valid PHP constant')); + } + } elseif ($xml['attribs']['type'] == 'package-info') { + if (in_array($xml['attribs']['to'], + array('name', 'summary', 'channel', 'notes', 'extends', 'description', + 'release_notes', 'license', 'release-license', 'license-uri', + 'version', 'api-version', 'state', 'api-state', 'release_date', + 'date', 'time'))) { + return true; + } else { + return array(PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'to', $xml['attribs']['to'], + array('name', 'summary', 'channel', 'notes', 'extends', 'description', + 'release_notes', 'license', 'release-license', 'license-uri', + 'version', 'api-version', 'state', 'api-state', 'release_date', + 'date', 'time')); + } + } else { + return array(PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'type', $xml['attribs']['type'], + array('pear-config', 'package-info', 'php-const')); + } + return true; + } + + /** + * Initialize a task instance with the parameters + * @param array raw, parsed xml + * @param unused + */ + function init($xml, $attribs) + { + $this->_replacements = isset($xml['attribs']) ? array($xml) : $xml; + } + + /** + * Do a package.xml 1.0 replacement, with additional package-info fields available + * + * See validateXml() source for the complete list of allowed fields + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param string file contents + * @param string the eventual final file location (informational only) + * @return string|false|PEAR_Error false to skip this file, PEAR_Error to fail + * (use $this->throwError), otherwise return the new contents + */ + function startSession($pkg, $contents, $dest) + { + $subst_from = $subst_to = array(); + foreach ($this->_replacements as $a) { + $a = $a['attribs']; + $to = ''; + if ($a['type'] == 'pear-config') { + if ($this->installphase == PEAR_TASK_PACKAGE) { + return false; + } + if ($a['to'] == 'master_server') { + $chan = $this->registry->getChannel($pkg->getChannel()); + if (!PEAR::isError($chan)) { + $to = $chan->getServer(); + } else { + $this->logger->log(0, "$dest: invalid pear-config replacement: $a[to]"); + return false; + } + } else { + if ($this->config->isDefinedLayer('ftp')) { + // try the remote config file first + $to = $this->config->get($a['to'], 'ftp', $pkg->getChannel()); + if (is_null($to)) { + // then default to local + $to = $this->config->get($a['to'], null, $pkg->getChannel()); + } + } else { + $to = $this->config->get($a['to'], null, $pkg->getChannel()); + } + } + if (is_null($to)) { + $this->logger->log(0, "$dest: invalid pear-config replacement: $a[to]"); + return false; + } + } elseif ($a['type'] == 'php-const') { + if ($this->installphase == PEAR_TASK_PACKAGE) { + return false; + } + if (defined($a['to'])) { + $to = constant($a['to']); + } else { + $this->logger->log(0, "$dest: invalid php-const replacement: $a[to]"); + return false; + } + } else { + if ($t = $pkg->packageInfo($a['to'])) { + $to = $t; + } else { + $this->logger->log(0, "$dest: invalid package-info replacement: $a[to]"); + return false; + } + } + if (!is_null($to)) { + $subst_from[] = $a['from']; + $subst_to[] = $to; + } + } + $this->logger->log(3, "doing " . sizeof($subst_from) . + " substitution(s) for $dest"); + if (sizeof($subst_from)) { + $contents = str_replace($subst_from, $subst_to, $contents); + } + return $contents; + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Replace/rw.php b/PEAR/Task/Replace/rw.php new file mode 100644 index 0000000..fd03d32 --- /dev/null +++ b/PEAR/Task/Replace/rw.php @@ -0,0 +1,67 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: rw.php,v 1.3 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a10 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Replace.php'; +/** + * Abstracts the replace task xml. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a10 + */ +class PEAR_Task_Replace_rw extends PEAR_Task_Replace +{ + function PEAR_Task_Replace_rw(&$pkg, &$config, &$logger, $fileXml) + { + parent::PEAR_Task_Common($config, $logger, PEAR_TASK_PACKAGE); + $this->_contents = $fileXml; + $this->_pkg = &$pkg; + $this->_params = array(); + } + + function validate() + { + return $this->validateXml($this->_pkg, $this->_params, $this->config, $this->_contents); + } + + function setInfo($from, $to, $type) + { + $this->_params = array('attribs' => array('from' => $from, 'to' => $to, 'type' => $type)); + } + + function getName() + { + return 'replace'; + } + + function getXml() + { + return $this->_params; + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Unixeol.php b/PEAR/Task/Unixeol.php new file mode 100644 index 0000000..6e9ab86 --- /dev/null +++ b/PEAR/Task/Unixeol.php @@ -0,0 +1,83 @@ + + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Unixeol.php,v 1.8 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Common.php'; +/** + * Implements the unix line endings file task. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Task_Unixeol extends PEAR_Task_Common +{ + var $type = 'simple'; + var $phase = PEAR_TASK_PACKAGE; + var $_replacements; + + /** + * Validate the raw xml at parsing-time. + * @param PEAR_PackageFile_v2 + * @param array raw, parsed xml + * @param PEAR_Config + * @static + */ + function validateXml($pkg, $xml, &$config, $fileXml) + { + if ($xml != '') { + return array(PEAR_TASK_ERROR_INVALID, 'no attributes allowed'); + } + return true; + } + + /** + * Initialize a task instance with the parameters + * @param array raw, parsed xml + * @param unused + */ + function init($xml, $attribs) + { + } + + /** + * Replace all line endings with line endings customized for the current OS + * + * See validateXml() source for the complete list of allowed fields + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param string file contents + * @param string the eventual final file location (informational only) + * @return string|false|PEAR_Error false to skip this file, PEAR_Error to fail + * (use $this->throwError), otherwise return the new contents + */ + function startSession($pkg, $contents, $dest) + { + $this->logger->log(3, "replacing all line endings with \\n in $dest"); + return preg_replace("/\r\n|\n\r|\r|\n/", "\n", $contents); + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Unixeol/rw.php b/PEAR/Task/Unixeol/rw.php new file mode 100644 index 0000000..80a178a --- /dev/null +++ b/PEAR/Task/Unixeol/rw.php @@ -0,0 +1,62 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: rw.php,v 1.4 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a10 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Unixeol.php'; +/** + * Abstracts the unixeol task xml. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a10 + */ +class PEAR_Task_Unixeol_rw extends PEAR_Task_Unixeol +{ + function PEAR_Task_Unixeol_rw(&$pkg, &$config, &$logger, $fileXml) + { + parent::PEAR_Task_Common($config, $logger, PEAR_TASK_PACKAGE); + $this->_contents = $fileXml; + $this->_pkg = &$pkg; + $this->_params = array(); + } + + function validate() + { + return true; + } + + function getName() + { + return 'unixeol'; + } + + function getXml() + { + return ''; + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Windowseol.php b/PEAR/Task/Windowseol.php new file mode 100644 index 0000000..6ab9854 --- /dev/null +++ b/PEAR/Task/Windowseol.php @@ -0,0 +1,83 @@ + + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Windowseol.php,v 1.7 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Common.php'; +/** + * Implements the windows line endsings file task. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Task_Windowseol extends PEAR_Task_Common +{ + var $type = 'simple'; + var $phase = PEAR_TASK_PACKAGE; + var $_replacements; + + /** + * Validate the raw xml at parsing-time. + * @param PEAR_PackageFile_v2 + * @param array raw, parsed xml + * @param PEAR_Config + * @static + */ + function validateXml($pkg, $xml, &$config, $fileXml) + { + if ($xml != '') { + return array(PEAR_TASK_ERROR_INVALID, 'no attributes allowed'); + } + return true; + } + + /** + * Initialize a task instance with the parameters + * @param array raw, parsed xml + * @param unused + */ + function init($xml, $attribs) + { + } + + /** + * Replace all line endings with windows line endings + * + * See validateXml() source for the complete list of allowed fields + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @param string file contents + * @param string the eventual final file location (informational only) + * @return string|false|PEAR_Error false to skip this file, PEAR_Error to fail + * (use $this->throwError), otherwise return the new contents + */ + function startSession($pkg, $contents, $dest) + { + $this->logger->log(3, "replacing all line endings with \\r\\n in $dest"); + return preg_replace("/\r\n|\n\r|\r|\n/", "\r\n", $contents); + } +} +?> \ No newline at end of file diff --git a/PEAR/Task/Windowseol/rw.php b/PEAR/Task/Windowseol/rw.php new file mode 100644 index 0000000..53d9ad0 --- /dev/null +++ b/PEAR/Task/Windowseol/rw.php @@ -0,0 +1,62 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: rw.php,v 1.4 2006/01/06 04:47:37 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a10 + */ +/** + * Base class + */ +require_once 'PEAR/Task/Windowseol.php'; +/** + * Abstracts the windowseol task xml. + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a10 + */ +class PEAR_Task_Windowseol_rw extends PEAR_Task_Windowseol +{ + function PEAR_Task_Windowseol_rw(&$pkg, &$config, &$logger, $fileXml) + { + parent::PEAR_Task_Common($config, $logger, PEAR_TASK_PACKAGE); + $this->_contents = $fileXml; + $this->_pkg = &$pkg; + $this->_params = array(); + } + + function validate() + { + return true; + } + + function getName() + { + return 'windowseol'; + } + + function getXml() + { + return ''; + } +} +?> \ No newline at end of file diff --git a/PEAR/Validate.php b/PEAR/Validate.php new file mode 100644 index 0000000..59e6820 --- /dev/null +++ b/PEAR/Validate.php @@ -0,0 +1,634 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Validate.php,v 1.51 2007/06/10 04:16:51 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/**#@+ + * Constants for install stage + */ +define('PEAR_VALIDATE_INSTALLING', 1); +define('PEAR_VALIDATE_UNINSTALLING', 2); // this is not bit-mapped like the others +define('PEAR_VALIDATE_NORMAL', 3); +define('PEAR_VALIDATE_DOWNLOADING', 4); // this is not bit-mapped like the others +define('PEAR_VALIDATE_PACKAGING', 7); +/**#@-*/ +require_once 'PEAR/Common.php'; +require_once 'PEAR/Validator/PECL.php'; + +/** + * Validation class for package.xml - channel-level advanced validation + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Validate +{ + var $packageregex = _PEAR_COMMON_PACKAGE_NAME_PREG; + /** + * @var PEAR_PackageFile_v1|PEAR_PackageFile_v2 + */ + var $_packagexml; + /** + * @var int one of the PEAR_VALIDATE_* constants + */ + var $_state = PEAR_VALIDATE_NORMAL; + /** + * Format: ('error' => array('field' => name, 'reason' => reason), 'warning' => same) + * @var array + * @access private + */ + var $_failures = array('error' => array(), 'warning' => array()); + + /** + * Override this method to handle validation of normal package names + * @param string + * @return bool + * @access protected + */ + function _validPackageName($name) + { + return (bool) preg_match('/^' . $this->packageregex . '\\z/', $name); + } + + /** + * @param string package name to validate + * @param string name of channel-specific validation package + * @final + */ + function validPackageName($name, $validatepackagename = false) + { + if ($validatepackagename) { + if (strtolower($name) == strtolower($validatepackagename)) { + return (bool) preg_match('/^[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*\\z/', $name); + } + } + return $this->_validPackageName($name); + } + + /** + * This validates a bundle name, and bundle names must conform + * to the PEAR naming convention, so the method is final and static. + * @param string + * @final + * @static + */ + function validGroupName($name) + { + return (bool) preg_match('/^' . _PEAR_COMMON_PACKAGE_NAME_PREG . '\\z/', $name); + } + + /** + * Determine whether $state represents a valid stability level + * @param string + * @return bool + * @static + * @final + */ + function validState($state) + { + return in_array($state, array('snapshot', 'devel', 'alpha', 'beta', 'stable')); + } + + /** + * Get a list of valid stability levels + * @return array + * @static + * @final + */ + function getValidStates() + { + return array('snapshot', 'devel', 'alpha', 'beta', 'stable'); + } + + /** + * Determine whether a version is a properly formatted version number that can be used + * by version_compare + * @param string + * @return bool + * @static + * @final + */ + function validVersion($ver) + { + return (bool) preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $ver); + } + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + */ + function setPackageFile(&$pf) + { + $this->_packagexml = &$pf; + } + + /** + * @access private + */ + function _addFailure($field, $reason) + { + $this->_failures['errors'][] = array('field' => $field, 'reason' => $reason); + } + + /** + * @access private + */ + function _addWarning($field, $reason) + { + $this->_failures['warnings'][] = array('field' => $field, 'reason' => $reason); + } + + function getFailures() + { + $failures = $this->_failures; + $this->_failures = array('warnings' => array(), 'errors' => array()); + return $failures; + } + + /** + * @param int one of the PEAR_VALIDATE_* constants + */ + function validate($state = null) + { + if (!isset($this->_packagexml)) { + return false; + } + if ($state !== null) { + $this->_state = $state; + } + $this->_failures = array('warnings' => array(), 'errors' => array()); + $this->validatePackageName(); + $this->validateVersion(); + $this->validateMaintainers(); + $this->validateDate(); + $this->validateSummary(); + $this->validateDescription(); + $this->validateLicense(); + $this->validateNotes(); + if ($this->_packagexml->getPackagexmlVersion() == '1.0') { + $this->validateState(); + $this->validateFilelist(); + } elseif ($this->_packagexml->getPackagexmlVersion() == '2.0' || + $this->_packagexml->getPackagexmlVersion() == '2.1') { + $this->validateTime(); + $this->validateStability(); + $this->validateDeps(); + $this->validateMainFilelist(); + $this->validateReleaseFilelist(); + //$this->validateGlobalTasks(); + $this->validateChangelog(); + } + return !((bool) count($this->_failures['errors'])); + } + + /** + * @access protected + */ + function validatePackageName() + { + if ($this->_state == PEAR_VALIDATE_PACKAGING || + $this->_state == PEAR_VALIDATE_NORMAL) { + if (($this->_packagexml->getPackagexmlVersion() == '2.0' || + $this->_packagexml->getPackagexmlVersion() == '2.1') && + $this->_packagexml->getExtends()) { + $version = $this->_packagexml->getVersion() . ''; + $name = $this->_packagexml->getPackage(); + $test = array_shift($a = explode('.', $version)); + if ($test == '0') { + return true; + } + $vlen = strlen($test); + $majver = substr($name, strlen($name) - $vlen); + while ($majver && !is_numeric($majver{0})) { + $majver = substr($majver, 1); + } + if ($majver != $test) { + $this->_addWarning('package', "package $name extends package " . + $this->_packagexml->getExtends() . ' and so the name should ' . + 'have a postfix equal to the major version like "' . + $this->_packagexml->getExtends() . $test . '"'); + return true; + } elseif (substr($name, 0, strlen($name) - $vlen) != + $this->_packagexml->getExtends()) { + $this->_addWarning('package', "package $name extends package " . + $this->_packagexml->getExtends() . ' and so the name must ' . + 'be an extension like "' . $this->_packagexml->getExtends() . + $test . '"'); + return true; + } + } + } + if (!$this->validPackageName($this->_packagexml->getPackage())) { + $this->_addFailure('name', 'package name "' . + $this->_packagexml->getPackage() . '" is invalid'); + return false; + } + } + + /** + * @access protected + */ + function validateVersion() + { + if ($this->_state != PEAR_VALIDATE_PACKAGING) { + if (!$this->validVersion($this->_packagexml->getVersion())) { + $this->_addFailure('version', + 'Invalid version number "' . $this->_packagexml->getVersion() . '"'); + } + return false; + } + $version = $this->_packagexml->getVersion(); + $versioncomponents = explode('.', $version); + if (count($versioncomponents) != 3) { + $this->_addWarning('version', + 'A version number should have 3 decimals (x.y.z)'); + return true; + } + $name = $this->_packagexml->getPackage(); + // version must be based upon state + switch ($this->_packagexml->getState()) { + case 'snapshot' : + return true; + case 'devel' : + if ($versioncomponents[0] . 'a' == '0a') { + return true; + } + if ($versioncomponents[0] == 0) { + $versioncomponents[0] = '0'; + $this->_addWarning('version', + 'version "' . $version . '" should be "' . + implode('.' ,$versioncomponents) . '"'); + } else { + $this->_addWarning('version', + 'packages with devel stability must be < version 1.0.0'); + } + return true; + break; + case 'alpha' : + case 'beta' : + // check for a package that extends a package, + // like Foo and Foo2 + if ($this->_state == PEAR_VALIDATE_PACKAGING) { + if (substr($versioncomponents[2], 1, 2) == 'rc') { + $this->_addFailure('version', 'Release Candidate versions ' . + 'must have capital RC, not lower-case rc'); + return false; + } + } + if (!$this->_packagexml->getExtends()) { + if ($versioncomponents[0] == '1') { + if ($versioncomponents[2]{0} == '0') { + if ($versioncomponents[2] == '0') { + // version 1.*.0000 + $this->_addWarning('version', + 'version 1.' . $versioncomponents[1] . + '.0 probably should not be alpha or beta'); + return true; + } elseif (strlen($versioncomponents[2]) > 1) { + // version 1.*.0RC1 or 1.*.0beta24 etc. + return true; + } else { + // version 1.*.0 + $this->_addWarning('version', + 'version 1.' . $versioncomponents[1] . + '.0 probably should not be alpha or beta'); + return true; + } + } else { + $this->_addWarning('version', + 'bugfix versions (1.3.x where x > 0) probably should ' . + 'not be alpha or beta'); + return true; + } + } elseif ($versioncomponents[0] != '0') { + $this->_addWarning('version', + 'major versions greater than 1 are not allowed for packages ' . + 'without an tag or an identical postfix (foo2 v2.0.0)'); + return true; + } + if ($versioncomponents[0] . 'a' == '0a') { + return true; + } + if ($versioncomponents[0] == 0) { + $versioncomponents[0] = '0'; + $this->_addWarning('version', + 'version "' . $version . '" should be "' . + implode('.' ,$versioncomponents) . '"'); + } + } else { + $vlen = strlen($versioncomponents[0] . ''); + $majver = substr($name, strlen($name) - $vlen); + while ($majver && !is_numeric($majver{0})) { + $majver = substr($majver, 1); + } + if (($versioncomponents[0] != 0) && $majver != $versioncomponents[0]) { + $this->_addWarning('version', 'first version number "' . + $versioncomponents[0] . '" must match the postfix of ' . + 'package name "' . $name . '" (' . + $majver . ')'); + return true; + } + if ($versioncomponents[0] == $majver) { + if ($versioncomponents[2]{0} == '0') { + if ($versioncomponents[2] == '0') { + // version 2.*.0000 + $this->_addWarning('version', + "version $majver." . $versioncomponents[1] . + '.0 probably should not be alpha or beta'); + return false; + } elseif (strlen($versioncomponents[2]) > 1) { + // version 2.*.0RC1 or 2.*.0beta24 etc. + return true; + } else { + // version 2.*.0 + $this->_addWarning('version', + "version $majver." . $versioncomponents[1] . + '.0 cannot be alpha or beta'); + return true; + } + } else { + $this->_addWarning('version', + "bugfix versions ($majver.x.y where y > 0) should " . + 'not be alpha or beta'); + return true; + } + } elseif ($versioncomponents[0] != '0') { + $this->_addWarning('version', + "only versions 0.x.y and $majver.x.y are allowed for alpha/beta releases"); + return true; + } + if ($versioncomponents[0] . 'a' == '0a') { + return true; + } + if ($versioncomponents[0] == 0) { + $versioncomponents[0] = '0'; + $this->_addWarning('version', + 'version "' . $version . '" should be "' . + implode('.' ,$versioncomponents) . '"'); + } + } + return true; + break; + case 'stable' : + if ($versioncomponents[0] == '0') { + $this->_addWarning('version', 'versions less than 1.0.0 cannot ' . + 'be stable'); + return true; + } + if (!is_numeric($versioncomponents[2])) { + if (preg_match('/\d+(rc|a|alpha|b|beta)\d*/i', + $versioncomponents[2])) { + $this->_addWarning('version', 'version "' . $version . '" or any ' . + 'RC/beta/alpha version cannot be stable'); + return true; + } + } + // check for a package that extends a package, + // like Foo and Foo2 + if ($this->_packagexml->getExtends()) { + $vlen = strlen($versioncomponents[0] . ''); + $majver = substr($name, strlen($name) - $vlen); + while ($majver && !is_numeric($majver{0})) { + $majver = substr($majver, 1); + } + if (($versioncomponents[0] != 0) && $majver != $versioncomponents[0]) { + $this->_addWarning('version', 'first version number "' . + $versioncomponents[0] . '" must match the postfix of ' . + 'package name "' . $name . '" (' . + $majver . ')'); + return true; + } + } elseif ($versioncomponents[0] > 1) { + $this->_addWarning('version', 'major version x in x.y.z may not be greater than ' . + '1 for any package that does not have an tag'); + } + return true; + break; + default : + return false; + break; + } + } + + /** + * @access protected + */ + function validateMaintainers() + { + // maintainers can only be truly validated server-side for most channels + // but allow this customization for those who wish it + return true; + } + + /** + * @access protected + */ + function validateDate() + { + if ($this->_state == PEAR_VALIDATE_NORMAL || + $this->_state == PEAR_VALIDATE_PACKAGING) { + + if (!preg_match('/(\d\d\d\d)\-(\d\d)\-(\d\d)/', + $this->_packagexml->getDate(), $res) || + count($res) < 4 + || !checkdate($res[2], $res[3], $res[1]) + ) { + $this->_addFailure('date', 'invalid release date "' . + $this->_packagexml->getDate() . '"'); + return false; + } + + + if ($this->_state == PEAR_VALIDATE_PACKAGING && + $this->_packagexml->getDate() != date('Y-m-d')) { + $this->_addWarning('date', 'Release Date "' . + $this->_packagexml->getDate() . '" is not today'); + } + } + return true; + } + + /** + * @access protected + */ + function validateTime() + { + if (!$this->_packagexml->getTime()) { + // default of no time value set + return true; + } + // packager automatically sets time, so only validate if + // pear validate is called + if ($this->_state = PEAR_VALIDATE_NORMAL) { + if (!preg_match('/\d\d:\d\d:\d\d/', + $this->_packagexml->getTime())) { + $this->_addFailure('time', 'invalid release time "' . + $this->_packagexml->getTime() . '"'); + return false; + } + if (strtotime($this->_packagexml->getTime()) == -1) { + $this->_addFailure('time', 'invalid release time "' . + $this->_packagexml->getTime() . '"'); + return false; + } + } + return true; + } + + /** + * @access protected + */ + function validateState() + { + // this is the closest to "final" php4 can get + if (!PEAR_Validate::validState($this->_packagexml->getState())) { + if (strtolower($this->_packagexml->getState() == 'rc')) { + $this->_addFailure('state', 'RC is not a state, it is a version ' . + 'postfix, use ' . $this->_packagexml->getVersion() . 'RC1, state beta'); + } + $this->_addFailure('state', 'invalid release state "' . + $this->_packagexml->getState() . '", must be one of: ' . + implode(', ', PEAR_Validate::getValidStates())); + return false; + } + return true; + } + + /** + * @access protected + */ + function validateStability() + { + $ret = true; + $packagestability = $this->_packagexml->getState(); + $apistability = $this->_packagexml->getState('api'); + if (!PEAR_Validate::validState($packagestability)) { + $this->_addFailure('state', 'invalid release stability "' . + $this->_packagexml->getState() . '", must be one of: ' . + implode(', ', PEAR_Validate::getValidStates())); + $ret = false; + } + $apistates = PEAR_Validate::getValidStates(); + array_shift($apistates); // snapshot is not allowed + if (!in_array($apistability, $apistates)) { + $this->_addFailure('state', 'invalid API stability "' . + $this->_packagexml->getState('api') . '", must be one of: ' . + implode(', ', $apistates)); + $ret = false; + } + return $ret; + } + + /** + * @access protected + */ + function validateSummary() + { + return true; + } + + /** + * @access protected + */ + function validateDescription() + { + return true; + } + + /** + * @access protected + */ + function validateLicense() + { + return true; + } + + /** + * @access protected + */ + function validateNotes() + { + return true; + } + + /** + * for package.xml 2.0 only - channels can't use package.xml 1.0 + * @access protected + */ + function validateDependencies() + { + return true; + } + + /** + * for package.xml 1.0 only + * @access private + */ + function _validateFilelist() + { + return true; // placeholder for now + } + + /** + * for package.xml 2.0 only + * @access protected + */ + function validateMainFilelist() + { + return true; // placeholder for now + } + + /** + * for package.xml 2.0 only + * @access protected + */ + function validateReleaseFilelist() + { + return true; // placeholder for now + } + + /** + * @access protected + */ + function validateChangelog() + { + return true; + } + + /** + * @access protected + */ + function validateFilelist() + { + return true; + } + + /** + * @access protected + */ + function validateDeps() + { + return true; + } +} +?> diff --git a/PEAR/Validator/PECL.php b/PEAR/Validator/PECL.php new file mode 100644 index 0000000..628d42d --- /dev/null +++ b/PEAR/Validator/PECL.php @@ -0,0 +1,63 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: PECL.php,v 1.8 2006/05/12 02:38:58 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a5 + */ +/** + * This is the parent class for all validators + */ +require_once 'PEAR/Validate.php'; +/** + * Channel Validator for the pecl.php.net channel + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a5 + */ +class PEAR_Validator_PECL extends PEAR_Validate +{ + function validateVersion() + { + if ($this->_state == PEAR_VALIDATE_PACKAGING) { + $version = $this->_packagexml->getVersion(); + $versioncomponents = explode('.', $version); + $last = array_pop($versioncomponents); + if (substr($last, 1, 2) == 'rc') { + $this->_addFailure('version', 'Release Candidate versions must have ' . + 'upper-case RC, not lower-case rc'); + return false; + } + } + return true; + } + + function validatePackageName() + { + $ret = parent::validatePackageName(); + if ($this->_packagexml->getPackageType() == 'extsrc' || + $this->_packagexml->getPackageType() == 'zendextsrc') { + if (strtolower($this->_packagexml->getPackage()) != + strtolower($this->_packagexml->getProvidesExtension())) { + $this->_addWarning('providesextension', 'package name "' . + $this->_packagexml->getPackage() . '" is different from extension name "' . + $this->_packagexml->getProvidesExtension() . '"'); + } + } + return $ret; + } +} +?> \ No newline at end of file diff --git a/PEAR/XMLParser.php b/PEAR/XMLParser.php new file mode 100644 index 0000000..5ef6626 --- /dev/null +++ b/PEAR/XMLParser.php @@ -0,0 +1,261 @@ + + * @author Stephan Schmidt (original XML_Unserializer code) + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: XMLParser.php,v 1.12 2006/03/27 04:39:03 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Parser for any xml file + * @category pear + * @package PEAR + * @author Greg Beaver + * @author Stephan Schmidt (original XML_Unserializer code) + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.6.1 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_XMLParser +{ + /** + * unserilialized data + * @var string $_serializedData + */ + var $_unserializedData = null; + + /** + * name of the root tag + * @var string $_root + */ + var $_root = null; + + /** + * stack for all data that is found + * @var array $_dataStack + */ + var $_dataStack = array(); + + /** + * stack for all values that are generated + * @var array $_valStack + */ + var $_valStack = array(); + + /** + * current tag depth + * @var int $_depth + */ + var $_depth = 0; + + /** + * @return array + */ + function getData() + { + return $this->_unserializedData; + } + + /** + * @param string xml content + * @return true|PEAR_Error + */ + function parse($data) + { + if (!extension_loaded('xml')) { + include_once 'PEAR.php'; + return PEAR::raiseError("XML Extension not found", 1); + } + $this->_valStack = array(); + $this->_dataStack = array(); + $this->_depth = 0; + + if (version_compare(phpversion(), '5.0.0', 'lt')) { + if (strpos($data, 'encoding="UTF-8"')) { + $data = utf8_decode($data); + } + $xp = xml_parser_create('ISO-8859-1'); + } else { + if (strpos($data, 'encoding="UTF-8"')) { + $xp = xml_parser_create('UTF-8'); + } else { + $xp = xml_parser_create('ISO-8859-1'); + } + } + xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, 0); + xml_set_object($xp, $this); + xml_set_element_handler($xp, 'startHandler', 'endHandler'); + xml_set_character_data_handler($xp, 'cdataHandler'); + if (!xml_parse($xp, $data)) { + $msg = xml_error_string(xml_get_error_code($xp)); + $line = xml_get_current_line_number($xp); + xml_parser_free($xp); + include_once 'PEAR.php'; + return PEAR::raiseError("XML Error: '$msg' on line '$line'", 2); + } + xml_parser_free($xp); + return true; + } + + /** + * Start element handler for XML parser + * + * @access private + * @param object $parser XML parser object + * @param string $element XML element + * @param array $attribs attributes of XML tag + * @return void + */ + function startHandler($parser, $element, $attribs) + { + $type = 'string'; + + $this->_depth++; + $this->_dataStack[$this->_depth] = null; + + $val = array( + 'name' => $element, + 'value' => null, + 'type' => $type, + 'childrenKeys' => array(), + 'aggregKeys' => array() + ); + + if (count($attribs) > 0) { + $val['children'] = array(); + $val['type'] = 'array'; + + $val['children']['attribs'] = $attribs; + + } + + array_push($this->_valStack, $val); + } + + /** + * post-process data + * + * @param string $data + * @param string $element element name + */ + function postProcess($data, $element) + { + return trim($data); + } + + /** + * End element handler for XML parser + * + * @access private + * @param object XML parser object + * @param string + * @return void + */ + function endHandler($parser, $element) + { + $value = array_pop($this->_valStack); + $data = $this->postProcess($this->_dataStack[$this->_depth], $element); + + // adjust type of the value + switch(strtolower($value['type'])) { + + /* + * unserialize an array + */ + case 'array': + if ($data !== '') { + $value['children']['_content'] = $data; + } + if (isset($value['children'])) { + $value['value'] = $value['children']; + } else { + $value['value'] = array(); + } + break; + + /* + * unserialize a null value + */ + case 'null': + $data = null; + break; + + /* + * unserialize any scalar value + */ + default: + settype($data, $value['type']); + $value['value'] = $data; + break; + } + $parent = array_pop($this->_valStack); + if ($parent === null) { + $this->_unserializedData = &$value['value']; + $this->_root = &$value['name']; + return true; + } else { + // parent has to be an array + if (!isset($parent['children']) || !is_array($parent['children'])) { + $parent['children'] = array(); + if ($parent['type'] != 'array') { + $parent['type'] = 'array'; + } + } + + if (!empty($value['name'])) { + // there already has been a tag with this name + if (in_array($value['name'], $parent['childrenKeys'])) { + // no aggregate has been created for this tag + if (!in_array($value['name'], $parent['aggregKeys'])) { + if (isset($parent['children'][$value['name']])) { + $parent['children'][$value['name']] = array($parent['children'][$value['name']]); + } else { + $parent['children'][$value['name']] = array(); + } + array_push($parent['aggregKeys'], $value['name']); + } + array_push($parent['children'][$value['name']], $value['value']); + } else { + $parent['children'][$value['name']] = &$value['value']; + array_push($parent['childrenKeys'], $value['name']); + } + } else { + array_push($parent['children'],$value['value']); + } + array_push($this->_valStack, $parent); + } + + $this->_depth--; + } + + /** + * Handler for character data + * + * @access private + * @param object XML parser object + * @param string CDATA + * @return void + */ + function cdataHandler($parser, $cdata) + { + $this->_dataStack[$this->_depth] .= $cdata; + } +} +?> \ No newline at end of file diff --git a/Pager.php b/Pager.php new file mode 100644 index 0000000..5a439a9 --- /dev/null +++ b/Pager.php @@ -0,0 +1,193 @@ + + * @author Richard Heyes + * @copyright 2003-2006 Lorenzo Alberton, Richard Heyes + * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) + * @version CVS: $Id: Pager.php,v 1.21 2006/05/22 10:29:26 quipo Exp $ + * @link http://pear.php.net/package/Pager + */ + +/** + * Pager - Wrapper class for [Sliding|Jumping]-window Pager + * Usage examples can be found in the PEAR manual + * + * @category HTML + * @package Pager + * @author Lorenzo Alberton + * @author Richard Heyes , + * @copyright 2003-2005 Lorenzo Alberton, Richard Heyes + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Pager + */ +class Pager +{ + // {{{ Pager() + + /** + * Constructor + * + * ------------------------------------------------------------------------- + * VALID options are (default values are set some lines before): + * - mode (string): "Jumping" or "Sliding" -window - It determines + * pager behaviour. See the manual for more details + * - totalItems (int): # of items to page. + * - perPage (int): # of items per page. + * - delta (int): # of page #s to show before and after the current + * one + * - linkClass (string): name of CSS class used for link styling. + * - append (bool): if true pageID is appended as GET value to the + * URL - if false it is embedded in the URL + * according to "fileName" specs + * - httpMethod (string): Specifies the HTTP method to use. Valid values + * are 'GET' or 'POST' + * according to "fileName" specs + * - importQuery (bool): if true (default behaviour), variables and + * values are imported from the submitted data + * (query string) and used in the generated links + * otherwise they're ignored completely + * - path (string): complete path to the page (without the page name) + * - fileName (string): name of the page, with a %d if append=true + * - urlVar (string): name of pageNumber URL var, for example "pageID" + * - altPrev (string): alt text to display for prev page, on prev link. + * - altNext (string): alt text to display for next page, on next link. + * - altPage (string): alt text to display before the page number. + * - prevImg (string): sth (it can be text such as "<< PREV" or an + * as well...) to display instead of "<<". + * - nextImg (string): same as prevImg, used for NEXT link, instead of + * the default value, which is ">>". + * - separator (string): what to use to separate numbers (can be an + * , a comma, an hyphen, or whatever. + * - spacesBeforeSeparator + * (int): number of spaces before the separator. + * - firstPagePre (string): + * string used before first page number (can be an + * , a "{", an empty string, or whatever. + * - firstPageText (string): + * string used in place of first page number + * - firstPagePost (string): + * string used after first page number (can be an + * , a "}", an empty string, or whatever. + * - lastPagePre (string): + * similar to firstPagePre. + * - lastPageText (string): + * similar to firstPageText. + * - lastPagePost (string): + * similar to firstPagePost. + * - spacesAfterSeparator + * (int): number of spaces after the separator. + * - firstLinkTitle (string): + * string used as title in tag + * - lastLinkTitle (string): + * string used as title in tag + * - prevLinkTitle (string): + * string used as title in tag + * - nextLinkTitle (string): + * string used as title in tag + * - curPageLinkClassName + * (string): name of CSS class used for current page link. + * - clearIfVoid(bool): if there's only one page, don't display pager. + * - extraVars (array): additional URL vars to be added to the querystring + * - excludeVars (array): URL vars to be excluded in the querystring + * - itemData (array): array of items to page. + * - useSessions (bool): if true, number of items to display per page is + * stored in the $_SESSION[$_sessionVar] var. + * - closeSession (bool): if true, the session is closed just after R/W. + * - sessionVar (string): name of the session var for perPage value. + * A value != from default can be useful when + * using more than one Pager istance in the page. + * - pearErrorMode (constant): + * PEAR_ERROR mode for raiseError(). + * Default is PEAR_ERROR_RETURN. + * ------------------------------------------------------------------------- + * REQUIRED options are: + * - fileName IF append==false (default is true) + * - itemData OR totalItems (if itemData is set, totalItems is overwritten) + * ------------------------------------------------------------------------- + * + * @param mixed $options An associative array of option names and + * their values. + * @access public + */ + function Pager($options = array()) + { + //this check evaluates to true on 5.0.0RC-dev, + //so i'm using another one, for now... + //if (version_compare(phpversion(), '5.0.0') == -1) { + if (get_class($this) == 'pager') { //php4 lowers class names + // assign factoried method to this for PHP 4 + eval('$this = Pager::factory($options);'); + } else { //php5 is case sensitive + $msg = 'Pager constructor is deprecated.' + .' You must use the "Pager::factory($params)" method' + .' instead of "new Pager($params)"'; + trigger_error($msg, E_USER_ERROR); + } + } + + // }}} + // {{{ factory() + + /** + * Return a pager based on $mode and $options + * + * @param array $options Optional parameters for the storage class + * @return object Object Storage object + * @static + * @access public + */ + function &factory($options = array()) + { + $mode = (isset($options['mode']) ? ucfirst($options['mode']) : 'Jumping'); + $classname = 'Pager_' . $mode; + $classfile = 'Pager' . DIRECTORY_SEPARATOR . $mode . '.php'; + + // Attempt to include a custom version of the named class, but don't treat + // a failure as fatal. The caller may have already included their own + // version of the named class. + if (!class_exists($classname)) { + include_once $classfile; + } + + // If the class exists, return a new instance of it. + if (class_exists($classname)) { + $pager =& new $classname($options); + return $pager; + } + + $null = null; + return $null; + } + + // }}} +} +?> \ No newline at end of file diff --git a/Pager/Common.php b/Pager/Common.php new file mode 100644 index 0000000..11ac7d9 --- /dev/null +++ b/Pager/Common.php @@ -0,0 +1,1507 @@ + + * @author Richard Heyes + * @copyright 2003-2006 Lorenzo Alberton, Richard Heyes + * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) + * @version CVS: $Id: Common.php,v 1.57 2006/10/11 16:01:33 quipo Exp $ + * @link http://pear.php.net/package/Pager + */ + +/** + * Two constants used to guess the path- and file-name of the page + * when the user doesn't set any other value + */ +if (substr($_SERVER['PHP_SELF'], -1) == '/') { + $http = !empty($_SERVER['HTTPS']) ? 'https://' : 'http://'; + define('CURRENT_FILENAME', ''); + define('CURRENT_PATHNAME', $http.$_SERVER['HTTP_HOST'].str_replace('\\', '/', $_SERVER['PHP_SELF'])); +} else { + define('CURRENT_FILENAME', preg_replace('/(.*)\?.*/', '\\1', basename($_SERVER['PHP_SELF']))); + define('CURRENT_PATHNAME', str_replace('\\', '/', dirname($_SERVER['PHP_SELF']))); +} +/** + * Error codes + */ +define('PAGER_OK', 0); +define('ERROR_PAGER', -1); +define('ERROR_PAGER_INVALID', -2); +define('ERROR_PAGER_INVALID_PLACEHOLDER', -3); +define('ERROR_PAGER_INVALID_USAGE', -4); +define('ERROR_PAGER_NOT_IMPLEMENTED', -5); + +/** + * Pager_Common - Common base class for [Sliding|Jumping] Window Pager + * Extend this class to write a custom paging class + * + * @category HTML + * @package Pager + * @author Lorenzo Alberton + * @author Richard Heyes + * @copyright 2003-2005 Lorenzo Alberton, Richard Heyes + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Pager + */ +class Pager_Common +{ + // {{{ class vars + + /** + * @var integer number of items + * @access private + */ + var $_totalItems; + + /** + * @var integer number of items per page + * @access private + */ + var $_perPage = 10; + + /** + * @var integer number of page links for each window + * @access private + */ + var $_delta = 10; + + /** + * @var integer current page number + * @access private + */ + var $_currentPage = 1; + + /** + * @var integer total pages number + * @access private + */ + var $_totalPages = 1; + + /** + * @var string CSS class for links + * @access private + */ + var $_linkClass = ''; + + /** + * @var string wrapper for CSS class name + * @access private + */ + var $_classString = ''; + + /** + * @var string path name + * @access private + */ + var $_path = CURRENT_PATHNAME; + + /** + * @var string file name + * @access private + */ + var $_fileName = CURRENT_FILENAME; + + /** + * @var boolean If false, don't override the fileName option. Use at your own risk. + * @access private + */ + var $_fixFileName = true; + + /** + * @var boolean you have to use FALSE with mod_rewrite + * @access private + */ + var $_append = true; + + /** + * @var string specifies which HTTP method to use + * @access private + */ + var $_httpMethod = 'GET'; + + /** + * @var string specifies which HTML form to use + * @access private + */ + var $_formID = ''; + + /** + * @var boolean whether or not to import submitted data + * @access private + */ + var $_importQuery = true; + + /** + * @var string name of the querystring var for pageID + * @access private + */ + var $_urlVar = 'pageID'; + + /** + * @var array data to pass through the link + * @access private + */ + var $_linkData = array(); + + /** + * @var array additional URL vars + * @access private + */ + var $_extraVars = array(); + + /** + * @var array URL vars to ignore + * @access private + */ + var $_excludeVars = array(); + + /** + * @var boolean TRUE => expanded mode (for Pager_Sliding) + * @access private + */ + var $_expanded = true; + + /** + * @var boolean TRUE => show accesskey attribute on tags + * @access private + */ + var $_accesskey = false; + + /** + * @var string extra attributes for the tag + * @access private + */ + var $_attributes = ''; + + /** + * @var string alt text for "first page" (use "%d" placeholder for page number) + * @access private + */ + var $_altFirst = 'first page'; + + /** + * @var string alt text for "previous page" + * @access private + */ + var $_altPrev = 'previous page'; + + /** + * @var string alt text for "next page" + * @access private + */ + var $_altNext = 'next page'; + + /** + * @var string alt text for "last page" (use "%d" placeholder for page number) + * @access private + */ + var $_altLast = 'last page'; + + /** + * @var string alt text for "page" + * @access private + */ + var $_altPage = 'page'; + + /** + * @var string image/text to use as "prev" link + * @access private + */ + var $_prevImg = '<< Back'; + + /** + * @var string image/text to use as "next" link + * @access private + */ + var $_nextImg = 'Next >>'; + + /** + * @var string link separator + * @access private + */ + var $_separator = ''; + + /** + * @var integer number of spaces before separator + * @access private + */ + var $_spacesBeforeSeparator = 0; + + /** + * @var integer number of spaces after separator + * @access private + */ + var $_spacesAfterSeparator = 1; + + /** + * @var string CSS class name for current page link + * @access private + */ + var $_curPageLinkClassName = ''; + + /** + * @var string Text before current page link + * @access private + */ + var $_curPageSpanPre = ''; + + /** + * @var string Text after current page link + * @access private + */ + var $_curPageSpanPost = ''; + + /** + * @var string Text before first page link + * @access private + */ + var $_firstPagePre = '['; + + /** + * @var string Text to be used for first page link + * @access private + */ + var $_firstPageText = ''; + + /** + * @var string Text after first page link + * @access private + */ + var $_firstPagePost = ']'; + + /** + * @var string Text before last page link + * @access private + */ + var $_lastPagePre = '['; + + /** + * @var string Text to be used for last page link + * @access private + */ + var $_lastPageText = ''; + + /** + * @var string Text after last page link + * @access private + */ + var $_lastPagePost = ']'; + + /** + * @var string Will contain the HTML code for the spaces + * @access private + */ + var $_spacesBefore = ''; + + /** + * @var string Will contain the HTML code for the spaces + * @access private + */ + var $_spacesAfter = ''; + + /** + * @var string $_firstLinkTitle + * @access private + */ + var $_firstLinkTitle = 'first page'; + + /** + * @var string $_nextLinkTitle + * @access private + */ + var $_nextLinkTitle = 'next page'; + + /** + * @var string $_prevLinkTitle + * @access private + */ + var $_prevLinkTitle = 'previous page'; + + /** + * @var string $_lastLinkTitle + * @access private + */ + var $_lastLinkTitle = 'last page'; + + /** + * @var string Text to be used for the 'show all' option in the select box + * @access private + */ + var $_showAllText = ''; + + /** + * @var array data to be paged + * @access private + */ + var $_itemData = null; + + /** + * @var boolean If TRUE and there's only one page, links aren't shown + * @access private + */ + var $_clearIfVoid = true; + + /** + * @var boolean Use session for storing the number of items per page + * @access private + */ + var $_useSessions = false; + + /** + * @var boolean Close the session when finished reading/writing data + * @access private + */ + var $_closeSession = false; + + /** + * @var string name of the session var for number of items per page + * @access private + */ + var $_sessionVar = 'setPerPage'; + + /** + * Pear error mode (when raiseError is called) + * (see PEAR doc) + * + * @var int $_pearErrorMode + * @access private + */ + var $_pearErrorMode = null; + + // }}} + // {{{ public vars + + /** + * @var string Complete set of links + * @access public + */ + var $links = ''; + + /** + * @var string Complete set of link tags + * @access public + */ + var $linkTags = ''; + + /** + * @var array Array with a key => value pair representing + * page# => bool value (true if key==currentPageNumber). + * can be used for extreme customization. + * @access public + */ + var $range = array(); + + /** + * @var array list of available options (safety check) + * @access private + */ + var $_allowed_options = array( + 'totalItems', + 'perPage', + 'delta', + 'linkClass', + 'path', + 'fileName', + 'fixFileName', + 'append', + 'httpMethod', + 'formID', + 'importQuery', + 'urlVar', + 'altFirst', + 'altPrev', + 'altNext', + 'altLast', + 'altPage', + 'prevImg', + 'nextImg', + 'expanded', + 'accesskey', + 'attributes', + 'separator', + 'spacesBeforeSeparator', + 'spacesAfterSeparator', + 'curPageLinkClassName', + 'curPageSpanPre', + 'curPageSpanPost', + 'firstPagePre', + 'firstPageText', + 'firstPagePost', + 'lastPagePre', + 'lastPageText', + 'lastPagePost', + 'firstLinkTitle', + 'nextLinkTitle', + 'prevLinkTitle', + 'lastLinkTitle', + 'showAllText', + 'itemData', + 'clearIfVoid', + 'useSessions', + 'closeSession', + 'sessionVar', + 'pearErrorMode', + 'extraVars', + 'excludeVars', + 'currentPage', + ); + + // }}} + // {{{ build() + + /** + * Generate or refresh the links and paged data after a call to setOptions() + * + * @access public + */ + function build() + { + $msg = 'PEAR::Pager Error:' + .' function "build()" not implemented.'; + return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED); + } + + // }}} + // {{{ getPageData() + + /** + * Returns an array of current pages data + * + * @param $pageID Desired page ID (optional) + * @return array Page data + * @access public + */ + function getPageData($pageID = null) + { + $pageID = empty($pageID) ? $this->_currentPage : $pageID; + + if (!isset($this->_pageData)) { + $this->_generatePageData(); + } + if (!empty($this->_pageData[$pageID])) { + return $this->_pageData[$pageID]; + } + return array(); + } + + // }}} + // {{{ getPageIdByOffset() + + /** + * Returns pageID for given offset + * + * @param $index Offset to get pageID for + * @return int PageID for given offset + */ + function getPageIdByOffset($index) + { + $msg = 'PEAR::Pager Error:' + .' function "getPageIdByOffset()" not implemented.'; + return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED); + } + + // }}} + // {{{ getOffsetByPageId() + + /** + * Returns offsets for given pageID. Eg, if you + * pass it pageID one and your perPage limit is 10 + * it will return (1, 10). PageID of 2 would + * give you (11, 20). + * + * @param integer PageID to get offsets for + * @return array First and last offsets + * @access public + */ + function getOffsetByPageId($pageid = null) + { + $pageid = isset($pageid) ? $pageid : $this->_currentPage; + if (!isset($this->_pageData)) { + $this->_generatePageData(); + } + + if (isset($this->_pageData[$pageid]) || is_null($this->_itemData)) { + return array( + max(($this->_perPage * ($pageid - 1)) + 1, 1), + min($this->_totalItems, $this->_perPage * $pageid) + ); + } else { + return array(0, 0); + } + } + + // }}} + // {{{ getPageRangeByPageId() + + /** + * @param integer PageID to get offsets for + * @return array First and last offsets + */ + function getPageRangeByPageId($pageID) + { + $msg = 'PEAR::Pager Error:' + .' function "getPageRangeByPageId()" not implemented.'; + return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED); + } + + // }}} + // {{{ getLinks() + + /** + * Returns back/next/first/last and page links, + * both as ordered and associative array. + * + * NB: in original PEAR::Pager this method accepted two parameters, + * $back_html and $next_html. Now the only parameter accepted is + * an integer ($pageID), since the html text for prev/next links can + * be set in the factory. If a second parameter is provided, then + * the method act as it previously did. This hack was done to mantain + * backward compatibility only. + * + * @param integer $pageID Optional pageID. If specified, links + * for that page are provided instead of current one. [ADDED IN NEW PAGER VERSION] + * @param string $next_html HTML to put inside the next link [deprecated: use the factory instead] + * @return array back/next/first/last and page links + */ + function getLinks($pageID=null, $next_html='') + { + $msg = 'PEAR::Pager Error:' + .' function "getLinks()" not implemented.'; + return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED); + } + + // }}} + // {{{ getCurrentPageID() + + /** + * Returns ID of current page + * + * @return integer ID of current page + */ + function getCurrentPageID() + { + return $this->_currentPage; + } + + // }}} + // {{{ getNextPageID() + + /** + * Returns next page ID. If current page is last page + * this function returns FALSE + * + * @return mixed Next page ID + */ + function getNextPageID() + { + return ($this->getCurrentPageID() == $this->numPages() ? false : $this->getCurrentPageID() + 1); + } + + // }}} + // {{{ getPreviousPageID() + + /** + * Returns previous page ID. If current page is first page + * this function returns FALSE + * + * @return mixed Previous pages' ID + */ + function getPreviousPageID() + { + return $this->isFirstPage() ? false : $this->getCurrentPageID() - 1; + } + + // }}} + // {{{ numItems() + + /** + * Returns number of items + * + * @return int Number of items + */ + function numItems() + { + return $this->_totalItems; + } + + // }}} + // {{{ numPages() + + /** + * Returns number of pages + * + * @return int Number of pages + */ + function numPages() + { + return (int)$this->_totalPages; + } + + // }}} + // {{{ isFirstPage() + + /** + * Returns whether current page is first page + * + * @return bool First page or not + */ + function isFirstPage() + { + return ($this->_currentPage < 2); + } + + // }}} + // {{{ isLastPage() + + /** + * Returns whether current page is last page + * + * @return bool Last page or not + */ + function isLastPage() + { + return ($this->_currentPage == $this->_totalPages); + } + + // }}} + // {{{ isLastPageComplete() + + /** + * Returns whether last page is complete + * + * @return bool Last age complete or not + */ + function isLastPageComplete() + { + return !($this->_totalItems % $this->_perPage); + } + + // }}} + // {{{ _generatePageData() + + /** + * Calculates all page data + * @access private + */ + function _generatePageData() + { + // Been supplied an array of data? + if (!is_null($this->_itemData)) { + $this->_totalItems = count($this->_itemData); + } + $this->_totalPages = ceil((float)$this->_totalItems / (float)$this->_perPage); + $i = 1; + if (!empty($this->_itemData)) { + foreach ($this->_itemData as $key => $value) { + $this->_pageData[$i][$key] = $value; + if (count($this->_pageData[$i]) >= $this->_perPage) { + $i++; + } + } + } else { + $this->_pageData = array(); + } + + //prevent URL modification + $this->_currentPage = min($this->_currentPage, $this->_totalPages); + } + + // }}} + // {{{ _renderLink() + + /** + * Renders a link using the appropriate method + * + * @param altText Alternative text for this link (title property) + * @param linkText Text contained by this link + * @return string The link in string form + * @access private + */ + function _renderLink($altText, $linkText) + { + if ($this->_httpMethod == 'GET') { + if ($this->_append) { + $href = '?' . $this->_http_build_query_wrapper($this->_linkData); + } else { + $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName); + } + return sprintf('%s', + htmlentities($this->_url . $href), + empty($this->_classString) ? '' : ' '.$this->_classString, + empty($this->_attributes) ? '' : ' '.$this->_attributes, + empty($this->_accesskey) ? '' : ' accesskey="'.$this->_linkData[$this->_urlVar].'"', + $altText, + $linkText + ); + } elseif ($this->_httpMethod == 'POST') { + return sprintf("%s", + $this->_generateFormOnClick($this->_url, $this->_linkData), + empty($this->_classString) ? '' : ' '.$this->_classString, + empty($this->_attributes) ? '' : ' '.$this->_attributes, + empty($this->_accesskey) ? '' : ' accesskey=\''.$this->_linkData[$this->_urlVar].'\'', + $altText, + $linkText + ); + } + return ''; + } + + // }}} + // {{{ _generateFormOnClick() + + /** + * Mimics http_build_query() behavior in the way the data + * in $data will appear when it makes it back to the server. + * For example: + * $arr = array('array' => array(array('hello', 'world'), + * 'things' => array('stuff', 'junk')); + * http_build_query($arr) + * and _generateFormOnClick('foo.php', $arr) + * will yield + * $_REQUEST['array'][0][0] === 'hello' + * $_REQUEST['array'][0][1] === 'world' + * $_REQUEST['array']['things'][0] === 'stuff' + * $_REQUEST['array']['things'][1] === 'junk' + * + * However, instead of generating a query string, it generates + * Javascript to create and submit a form. + * + * @param string $formAction where the form should be submitted + * @param array $data the associative array of names and values + * @return string A string of javascript that generates a form and submits it + * @access private + */ + function _generateFormOnClick($formAction, $data) + { + // Check we have an array to work with + if (!is_array($data)) { + trigger_error( + '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.', + E_USER_WARNING + ); + return false; + } + + if (!empty($this->_formID)) { + $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; '; + } else { + $str = 'var form = document.createElement("form"); var input = ""; '; + } + + // We /shouldn't/ need to escape the URL ... + $str .= sprintf('form.action = "%s"; ', htmlentities($formAction)); + $str .= sprintf('form.method = "%s"; ', $this->_httpMethod); + foreach ($data as $key => $val) { + $str .= $this->_generateFormOnClickHelper($val, $key); + } + + if (empty($this->_formID)) { + $str .= 'document.getElementsByTagName("body")[0].appendChild(form);'; + } + + $str .= 'form.submit(); return false;'; + return $str; + } + + // }}} + // {{{ _generateFormOnClickHelper + + /** + * This is used by _generateFormOnClick(). + * Recursively processes the arrays, objects, and literal values. + * + * @param data Data that should be rendered + * @param prev The name so far + * @return string A string of Javascript that creates form inputs + * representing the data + * @access private + */ + function _generateFormOnClickHelper($data, $prev = '') + { + $str = ''; + if (is_array($data) || is_object($data)) { + // foreach key/visible member + foreach ((array)$data as $key => $val) { + // append [$key] to prev + $tempKey = sprintf('%s[%s]', $prev, $key); + $str .= $this->_generateFormOnClickHelper($val, $tempKey); + } + } else { // must be a literal value + // escape newlines and carriage returns + $search = array("\n", "\r"); + $replace = array('\n', '\n'); + $escapedData = str_replace($search, $replace, $data); + // am I forgetting any dangerous whitespace? + // would a regex be faster? + // if it's already encoded, don't encode it again + if (!$this->_isEncoded($escapedData)) { + $escapedData = urlencode($escapedData); + } + $escapedData = htmlentities($escapedData, ENT_QUOTES, 'UTF-8'); + + $str .= 'input = document.createElement("input"); '; + $str .= 'input.type = "hidden"; '; + $str .= sprintf('input.name = "%s"; ', $prev); + $str .= sprintf('input.value = "%s"; ', $escapedData); + $str .= 'form.appendChild(input); '; + } + return $str; + } + + // }}} + // {{{ _getLinksData() + + /** + * Returns the correct link for the back/pages/next links + * + * @return array Data + * @access private + */ + function _getLinksData() + { + $qs = array(); + if ($this->_importQuery) { + if ($this->_httpMethod == 'POST') { + $qs = $_POST; + } elseif ($this->_httpMethod == 'GET') { + $qs = $_GET; + } + } + foreach ($this->_excludeVars as $exclude) { + if (array_key_exists($exclude, $qs)) { + unset($qs[$exclude]); + } + } + if (count($this->_extraVars)){ + $this->_recursive_urldecode($this->_extraVars); + $qs = array_merge($qs, $this->_extraVars); + } + if (count($qs) && get_magic_quotes_gpc()){ + $this->_recursive_stripslashes($qs); + } + return $qs; + } + + // }}} + // {{{ _recursive_stripslashes() + + /** + * Helper method + * @param mixed $var + * @access private + */ + function _recursive_stripslashes(&$var) + { + if (is_array($var)) { + foreach (array_keys($var) as $k) { + $this->_recursive_stripslashes($var[$k]); + } + } else { + $var = stripslashes($var); + } + } + + // }}} + // {{{ _recursive_urldecode() + + /** + * Helper method + * @param mixed $var + * @access private + */ + function _recursive_urldecode(&$var) + { + if (is_array($var)) { + foreach (array_keys($var) as $k) { + $this->_recursive_urldecode($var[$k]); + } + } else { + $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES)); + $var = strtr($var, $trans_tbl); + } + } + + // }}} + // {{{ _getBackLink() + + /** + * Returns back link + * + * @param $url URL to use in the link [deprecated: use the factory instead] + * @param $link HTML to use as the link [deprecated: use the factory instead] + * @return string The link + * @access private + */ + function _getBackLink($url='', $link='') + { + //legacy settings... the preferred way to set an option + //now is passing it to the factory + if (!empty($url)) { + $this->_path = $url; + } + if (!empty($link)) { + $this->_prevImg = $link; + } + $back = ''; + if ($this->_currentPage > 1) { + $this->_linkData[$this->_urlVar] = $this->getPreviousPageID(); + $back = $this->_renderLink($this->_altPrev, $this->_prevImg) + . $this->_spacesBefore . $this->_spacesAfter; + } + return $back; + } + + // }}} + // {{{ _getPageLinks() + + /** + * Returns pages link + * + * @param $url URL to use in the link [deprecated: use the factory instead] + * @return string Links + * @access private + */ + function _getPageLinks($url='') + { + $msg = 'PEAR::Pager Error:' + .' function "_getPageLinks()" not implemented.'; + return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED); + } + + // }}} + // {{{ _getNextLink() + + /** + * Returns next link + * + * @param $url URL to use in the link [deprecated: use the factory instead] + * @param $link HTML to use as the link [deprecated: use the factory instead] + * @return string The link + * @access private + */ + function _getNextLink($url='', $link='') + { + //legacy settings... the preferred way to set an option + //now is passing it to the factory + if (!empty($url)) { + $this->_path = $url; + } + if (!empty($link)) { + $this->_nextImg = $link; + } + $next = ''; + if ($this->_currentPage < $this->_totalPages) { + $this->_linkData[$this->_urlVar] = $this->getNextPageID(); + $next = $this->_spacesAfter + . $this->_renderLink($this->_altNext, $this->_nextImg) + . $this->_spacesBefore . $this->_spacesAfter; + } + return $next; + } + + // }}} + // {{{ _getFirstLinkTag() + + /** + * @return string + * @access private + */ + function _getFirstLinkTag() + { + if ($this->isFirstPage() || ($this->_httpMethod != 'GET')) { + return ''; + } + return sprintf(''."\n", + $this->_getLinkTagUrl(1), + $this->_firstLinkTitle + ); + } + + // }}} + // {{{ _getPrevLinkTag() + + /** + * Returns previous link tag + * + * @return string the link tag + * @access private + */ + function _getPrevLinkTag() + { + if ($this->isFirstPage() || ($this->_httpMethod != 'GET')) { + return ''; + } + return sprintf(''."\n", + $this->_getLinkTagUrl($this->getPreviousPageID()), + $this->_prevLinkTitle + ); + } + + // }}} + // {{{ _getNextLinkTag() + + /** + * Returns next link tag + * + * @return string the link tag + * @access private + */ + function _getNextLinkTag() + { + if ($this->isLastPage() || ($this->_httpMethod != 'GET')) { + return ''; + } + return sprintf(''."\n", + $this->_getLinkTagUrl($this->getNextPageID()), + $this->_nextLinkTitle + ); + } + + // }}} + // {{{ _getLastLinkTag() + + /** + * @return string the link tag + * @access private + */ + function _getLastLinkTag() + { + if ($this->isLastPage() || ($this->_httpMethod != 'GET')) { + return ''; + } + return sprintf(''."\n", + $this->_getLinkTagUrl($this->_totalPages), + $this->_lastLinkTitle + ); + } + + // }}} + // {{{ _getLinkTagUrl() + + /** + * Helper method + * @return string the link tag url + * @access private + */ + function _getLinkTagUrl($pageID) + { + $this->_linkData[$this->_urlVar] = $pageID; + if ($this->_append) { + $href = '?' . $this->_http_build_query_wrapper($this->_linkData); + } else { + $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName); + } + return htmlentities($this->_url . $href); + } + + // }}} + // {{{ getPerPageSelectBox() + + /** + * Returns a string with a XHTML SELECT menu, + * useful for letting the user choose how many items per page should be + * displayed. If parameter useSessions is TRUE, this value is stored in + * a session var. The string isn't echoed right now so you can use it + * with template engines. + * + * @param integer $start + * @param integer $end + * @param integer $step + * @param boolean $showAllData If true, perPage is set equal to totalItems. + * @param array (or string $optionText for BC reasons) + * - 'optionText': text to show in each option. + * Use '%d' where you want to see the number of pages selected. + * - 'attributes': (html attributes) Tag attributes or + * HTML attributes (id="foo" pairs), will be inserted in the + * tag + * @return string xhtml select box + * @access public + */ + function getPageSelectBox($params = array(), $extraAttributes = '') + { + require_once 'Pager/HtmlWidgets.php'; + $widget =& new Pager_HtmlWidgets($this); + return $widget->getPageSelectBox($params, $extraAttributes); + } + + // }}} + // {{{ _printFirstPage() + + /** + * Print [1] + * + * @return string String with link to 1st page, + * or empty string if this is the 1st page. + * @access private + */ + function _printFirstPage() + { + if ($this->isFirstPage()) { + return ''; + } + $this->_linkData[$this->_urlVar] = 1; + return $this->_renderLink( + str_replace('%d', 1, $this->_altFirst), + $this->_firstPagePre . $this->_firstPageText . $this->_firstPagePost + ) . $this->_spacesBefore . $this->_spacesAfter; + } + + // }}} + // {{{ _printLastPage() + + /** + * Print [numPages()] + * + * @return string String with link to last page, + * or empty string if this is the 1st page. + * @access private + */ + function _printLastPage() + { + if ($this->isLastPage()) { + return ''; + } + $this->_linkData[$this->_urlVar] = $this->_totalPages; + return $this->_renderLink( + str_replace('%d', $this->_totalPages, $this->_altLast), + $this->_lastPagePre . $this->_lastPageText . $this->_lastPagePost + ); + } + + // }}} + // {{{ _setFirstLastText() + + /** + * sets the private _firstPageText, _lastPageText variables + * based on whether they were set in the options + * + * @access private + */ + function _setFirstLastText() + { + if ($this->_firstPageText == '') { + $this->_firstPageText = '1'; + } + if ($this->_lastPageText == '') { + $this->_lastPageText = $this->_totalPages; + } + } + + // }}} + // {{{ _http_build_query_wrapper() + + /** + * This is a slightly modified version of the http_build_query() function; + * it heavily borrows code from PHP_Compat's http_build_query(). + * The main change is the usage of htmlentities instead of urlencode, + * since it's too aggressive + * + * @author Stephan Schmidt + * @author Aidan Lister + * @author Lorenzo Alberton + * @param array $data + * @return string + * @access private + */ + function _http_build_query_wrapper($data) + { + $data = (array)$data; + if (empty($data)) { + return ''; + } + $separator = ini_get('arg_separator.output'); + if ($separator == '&') { + $separator = '&'; //the string is escaped by htmlentities anyway... + } + $tmp = array (); + foreach ($data as $key => $val) { + if (is_scalar($val)) { + //array_push($tmp, $key.'='.$val); + $val = urlencode($val); + array_push($tmp, $key .'='. str_replace('%2F', '/', $val)); + continue; + } + // If the value is an array, recursively parse it + if (is_array($val)) { + array_push($tmp, $this->__http_build_query($val, htmlentities($key))); + continue; + } + } + return implode($separator, $tmp); + } + + // }}} + // {{{ __http_build_query() + + /** + * Helper function + * @author Stephan Schmidt + * @author Aidan Lister + * @access private + */ + function __http_build_query($array, $name) + { + $tmp = array (); + $separator = ini_get('arg_separator.output'); + if ($separator == '&') { + $separator = '&'; //the string is escaped by htmlentities anyway... + } + foreach ($array as $key => $value) { + if (is_array($value)) { + //array_push($tmp, $this->__http_build_query($value, sprintf('%s[%s]', $name, $key))); + array_push($tmp, $this->__http_build_query($value, $name.'%5B'.$key.'%5D')); + } elseif (is_scalar($value)) { + //array_push($tmp, sprintf('%s[%s]=%s', $name, htmlentities($key), htmlentities($value))); + array_push($tmp, $name.'%5B'.htmlentities($key).'%5D='.htmlentities($value)); + } elseif (is_object($value)) { + //array_push($tmp, $this->__http_build_query(get_object_vars($value), sprintf('%s[%s]', $name, $key))); + array_push($tmp, $this->__http_build_query(get_object_vars($value), $name.'%5B'.$key.'%5D')); + } + } + return implode($separator, $tmp); + } + + // }}} + // {{{ _isEncoded() + + /** + * Helper function + * Check if a string is an encoded multibyte string + * @param string $string + * @return boolean + * @access private + */ + + function _isEncoded($string) + { + $hexchar = '&#[\dA-Fx]{2,};'; + return preg_match("/^(\s|($hexchar))*$/Uims", $string) ? true : false; + } + + // }}} + // {{{ raiseError() + + /** + * conditionally includes PEAR base class and raise an error + * + * @param string $msg Error message + * @param int $code Error code + * @access private + */ + function raiseError($msg, $code) + { + include_once 'PEAR.php'; + if (empty($this->_pearErrorMode)) { + $this->_pearErrorMode = PEAR_ERROR_RETURN; + } + return PEAR::raiseError($msg, $code, $this->_pearErrorMode); + } + + // }}} + // {{{ setOptions() + + /** + * Set and sanitize options + * + * @param mixed $options An associative array of option names and + * their values. + * @return integer error code (PAGER_OK on success) + * @access public + */ + function setOptions($options) + { + foreach ($options as $key => $value) { + if (in_array($key, $this->_allowed_options) && (!is_null($value))) { + $this->{'_' . $key} = $value; + } + } + + //autodetect http method + if (!isset($options['httpMethod']) + && !isset($_GET[$this->_urlVar]) + && isset($_POST[$this->_urlVar]) + ) { + $this->_httpMethod = 'POST'; + } else { + $this->_httpMethod = strtoupper($this->_httpMethod); + } + + $this->_fileName = ltrim($this->_fileName, '/'); //strip leading slash + $this->_path = rtrim($this->_path, '/'); //strip trailing slash + + if ($this->_append) { + if ($this->_fixFileName) { + $this->_fileName = CURRENT_FILENAME; //avoid possible user error; + } + $this->_url = $this->_path.'/'.$this->_fileName; + } else { + $this->_url = $this->_path; + if (strncasecmp($this->_fileName, 'javascript', 10) != 0) { + $this->_url .= '/'; + } + if (!strstr($this->_fileName, '%d')) { + trigger_error($this->errorMessage(ERROR_PAGER_INVALID_USAGE), E_USER_WARNING); + } + } + + $this->_classString = ''; + if (strlen($this->_linkClass)) { + $this->_classString = 'class="'.$this->_linkClass.'"'; + } + + if (strlen($this->_curPageLinkClassName)) { + $this->_curPageSpanPre = ''; + $this->_curPageSpanPost = ''; + } + + $this->_perPage = max($this->_perPage, 1); //avoid possible user errors + + if ($this->_useSessions && !isset($_SESSION)) { + session_start(); + } + if (!empty($_REQUEST[$this->_sessionVar])) { + $this->_perPage = max(1, (int)$_REQUEST[$this->_sessionVar]); + if ($this->_useSessions) { + $_SESSION[$this->_sessionVar] = $this->_perPage; + } + } + + if (!empty($_SESSION[$this->_sessionVar])) { + $this->_perPage = $_SESSION[$this->_sessionVar]; + } + + if ($this->_closeSession) { + session_write_close(); + } + + $this->_spacesBefore = str_repeat(' ', $this->_spacesBeforeSeparator); + $this->_spacesAfter = str_repeat(' ', $this->_spacesAfterSeparator); + + if (isset($_REQUEST[$this->_urlVar]) && empty($options['currentPage'])) { + $this->_currentPage = (int)$_REQUEST[$this->_urlVar]; + } + $this->_currentPage = max($this->_currentPage, 1); + $this->_linkData = $this->_getLinksData(); + + return PAGER_OK; + } + + // }}} + // {{{ getOption() + + /** + * Return the current value of a given option + * + * @param string option name + * @return mixed option value + */ + function getOption($name) + { + if (!in_array($name, $this->_allowed_options)) { + $msg = 'PEAR::Pager Error:' + .' invalid option: '.$name; + return $this->raiseError($msg, ERROR_PAGER_INVALID); + } + return $this->{'_' . $name}; + } + + // }}} + // {{{ getOptions() + + /** + * Return an array with all the current pager options + * + * @return array list of all the pager options + */ + function getOptions() + { + $options = array(); + foreach ($this->_allowed_options as $option) { + $options[$option] = $this->{'_' . $option}; + } + return $options; + } + + // }}} + // {{{ errorMessage() + + /** + * Return a textual error message for a PAGER error code + * + * @param int $code error code + * @return string error message + * @access public + */ + function errorMessage($code) + { + static $errorMessages; + if (!isset($errorMessages)) { + $errorMessages = array( + ERROR_PAGER => 'unknown error', + ERROR_PAGER_INVALID => 'invalid', + ERROR_PAGER_INVALID_PLACEHOLDER => 'invalid format - use "%d" as placeholder.', + ERROR_PAGER_INVALID_USAGE => 'if $options[\'append\'] is set to false, ' + .' $options[\'fileName\'] MUST contain the "%d" placeholder.', + ERROR_PAGER_NOT_IMPLEMENTED => 'not implemented' + ); + } + + return 'PEAR::Pager error: '. (isset($errorMessages[$code]) ? + $errorMessages[$code] : $errorMessages[ERROR_PAGER]); + } + + // }}} +} +?> \ No newline at end of file diff --git a/Pager/HtmlWidgets.php b/Pager/HtmlWidgets.php new file mode 100644 index 0000000..b406164 --- /dev/null +++ b/Pager/HtmlWidgets.php @@ -0,0 +1,229 @@ + + * @copyright 2003-2006 Lorenzo Alberton + * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) + * @version CVS: $Id: HtmlWidgets.php,v 1.2 2006/11/24 13:46:05 quipo Exp $ + * @link http://pear.php.net/package/Pager + */ + +/** + * Two constants used to guess the path- and file-name of the page + * when the user doesn't set any other value + */ +class Pager_HtmlWidgets +{ + var $pager = null; + + // {{{ constructor + + function Pager_HtmlWidgets(&$pager) + { + $this->pager =& $pager; + } + + // }}} + // {{{ getPerPageSelectBox() + + /** + * Returns a string with a XHTML SELECT menu, + * useful for letting the user choose how many items per page should be + * displayed. If parameter useSessions is TRUE, this value is stored in + * a session var. The string isn't echoed right now so you can use it + * with template engines. + * + * @param integer $start + * @param integer $end + * @param integer $step + * @param boolean $showAllData If true, perPage is set equal to totalItems. + * @param array (or string $optionText for BC reasons) + * - 'optionText': text to show in each option. + * Use '%d' where you want to see the number of pages selected. + * - 'attributes': (html attributes) Tag attributes or + * HTML attributes (id="foo" pairs), will be inserted in the + * '; + } + if ($showAllData && $last < $this->pager->_totalItems) { + $tmp .= ''; + } + $tmp .= ''; + return $tmp; + } + + // }}} + // {{{ getPageSelectBox() + + /** + * Returns a string with a XHTML SELECT menu with the page numbers, + * useful as an alternative to the links + * + * @param array - 'optionText': text to show in each option. + * Use '%d' where you want to see the number of pages selected. + * - 'autoSubmit': if TRUE, add some js code to submit the + * form on the onChange event + * @param string $extraAttributes (html attributes) Tag attributes or + * HTML attributes (id="foo" pairs), will be inserted in the + * pager->_httpMethod == 'GET') { + $selector = '\' + '.'this.options[this.selectedIndex].value + \''; + if ($this->pager->_append) { + $href = '?' . $this->pager->_http_build_query_wrapper($this->pager->_linkData); + $href = htmlentities($this->pager->_url). preg_replace( + '/(&|&|\?)('.$this->pager->_urlVar.'=)(\d+)/', + '\\1\\2'.$selector, + htmlentities($href) + ); + } else { + $href = htmlentities($this->pager->_url . str_replace('%d', $selector, $this->pager->_fileName)); + } + $tmp .= ' onchange="document.location.href=\'' + . $href .'\'' + . '"'; + } elseif ($this->pager->_httpMethod == 'POST') { + $tmp .= " onchange='" + . $this->pager->_generateFormOnClick($this->pager->_url, $this->pager->_linkData) + . "'"; + $tmp = preg_replace( + '/(input\.name = \"'.$this->pager->_urlVar.'\"; input\.value =) \"(\d+)\";/', + '\\1 this.options[this.selectedIndex].value;', + $tmp + ); + } + } + $tmp .= '>'; + $start = 1; + $end = $this->pager->numPages(); + $selected = $this->pager->getCurrentPageID(); + for ($i=$start; $i<=$end; $i++) { + $tmp .= '